use crate::core::config::manifest_extras::ManifestExtras;
use crate::core::hash::{self, CommentStyle};
use crate::core::template_versions as tv;
use std::collections::HashMap;
use std::fmt::Write as FmtWrite;
pub(super) fn render_csproj(
pkg_name: &str,
pkg_path: &str,
pkg_version: &str,
dep_mode: crate::e2e::config::DependencyMode,
extras: Option<&ManifestExtras>,
) -> String {
let pkg_ref = match dep_mode {
crate::e2e::config::DependencyMode::Registry => {
format!(" <PackageReference Include=\"{pkg_name}\" Version=\"{pkg_version}\" />")
}
crate::e2e::config::DependencyMode::Local => {
format!(" <ProjectReference Include=\"{pkg_path}\" />")
}
};
let extras_block = match extras {
Some(e) if !e.is_empty() => {
let mut lines = Vec::new();
let mut all_deps = e.dependencies.clone();
for (name, spec) in &e.dev_dependencies {
all_deps.insert(name.clone(), spec.clone());
}
if !all_deps.is_empty() {
for (name, spec) in &all_deps {
if let Some(version) = spec.version() {
lines.push(format!(
" <PackageReference Include=\"{name}\" Version=\"{version}\" />"
));
}
}
}
lines.join("\n")
}
_ => String::new(),
};
crate::e2e::template_env::render(
"csharp/csproj.jinja",
minijinja::context! {
pkg_ref => pkg_ref,
extras_block => extras_block,
namespace => pkg_name,
microsoft_net_test_sdk_version => tv::nuget::MICROSOFT_NET_TEST_SDK,
xunit_version => tv::nuget::XUNIT,
xunit_runner_version => tv::nuget::XUNIT_RUNNER_VISUALSTUDIO,
},
)
}
pub(super) fn render_test_setup(
needs_mock_server: bool,
test_documents_dir: &str,
namespace: &str,
env: &HashMap<String, String>,
) -> String {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::DoubleSlash));
out.push_str("using System;\n");
out.push_str("using System.IO;\n");
if needs_mock_server {
out.push_str("using System.Diagnostics;\n");
}
out.push_str("using System.Runtime.CompilerServices;\n\n");
let _ = writeln!(out, "namespace {namespace};\n");
out.push_str("internal static class TestSetup\n");
out.push_str("{\n");
if needs_mock_server {
out.push_str(" private static Process? _mockServer;\n\n");
}
out.push_str(" [ModuleInitializer]\n");
out.push_str(" internal static void Init()\n");
out.push_str(" {\n");
if !env.is_empty() {
let mut sorted_keys: Vec<_> = env.keys().collect();
sorted_keys.sort();
for key in sorted_keys {
let value = &env[key];
let _ = writeln!(
out,
" if (Environment.GetEnvironmentVariable(\"{key}\") == null) {{"
);
let _ = writeln!(
out,
" Environment.SetEnvironmentVariable(\"{key}\", \"{value}\");"
);
out.push_str(" }\n");
}
out.push('\n');
}
let _ = writeln!(
out,
" // Walk up from the assembly directory until we find the repo root."
);
let _ = writeln!(
out,
" // Prefer a sibling {test_documents_dir}/ directory (chdir into it so that"
);
out.push_str(" // fixture paths like \"docx/fake.docx\" resolve relative to it). If that\n");
out.push_str(" // is absent (projects with no document fixtures), fall\n");
out.push_str(" // back to a sibling alef.toml or fixtures/ marker as the repo root.\n");
out.push_str(" var dir = new DirectoryInfo(AppContext.BaseDirectory);\n");
out.push_str(" DirectoryInfo? repoRoot = null;\n");
out.push_str(" while (dir != null)\n");
out.push_str(" {\n");
let _ = writeln!(
out,
" var documentsCandidate = Path.Combine(dir.FullName, \"{test_documents_dir}\");"
);
out.push_str(" if (Directory.Exists(documentsCandidate))\n");
out.push_str(" {\n");
out.push_str(" repoRoot = dir;\n");
out.push_str(" Directory.SetCurrentDirectory(documentsCandidate);\n");
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" if (File.Exists(Path.Combine(dir.FullName, \"alef.toml\"))\n");
out.push_str(" || Directory.Exists(Path.Combine(dir.FullName, \"fixtures\")))\n");
out.push_str(" {\n");
out.push_str(" repoRoot = dir;\n");
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" dir = dir.Parent;\n");
out.push_str(" }\n");
if needs_mock_server {
out.push('\n');
let mock_server_code =
crate::e2e::template_env::render("csharp/test_setup_mock_server.cs.jinja", minijinja::context! {});
out.push_str(&mock_server_code);
}
out.push_str(" }\n");
out.push_str("}\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_test_setup_with_env_vars() {
let mut env = HashMap::new();
env.insert("ZEBRA_VAR".to_string(), "z_value".to_string());
env.insert("ALPHA_VAR".to_string(), "a_value".to_string());
env.insert("BETA_VAR".to_string(), "b_value".to_string());
let output = render_test_setup(false, "fixtures", "FixtureE2E", &env);
assert!(output.contains("ALPHA_VAR"));
assert!(output.contains("a_value"));
assert!(output.contains("BETA_VAR"));
assert!(output.contains("b_value"));
assert!(output.contains("ZEBRA_VAR"));
assert!(output.contains("z_value"));
let alpha_pos = output.find("ALPHA_VAR").unwrap();
let beta_pos = output.find("BETA_VAR").unwrap();
let zebra_pos = output.find("ZEBRA_VAR").unwrap();
assert!(alpha_pos < beta_pos && beta_pos < zebra_pos);
assert!(output.contains("Environment.SetEnvironmentVariable("));
}
#[test]
fn test_render_test_setup_empty_env() {
let env = HashMap::new();
let output = render_test_setup(false, "fixtures", "FixtureE2E", &env);
assert!(!output.contains("Environment.SetEnvironmentVariable("));
}
#[test]
fn test_render_test_setup_env_null_check() {
let mut env = HashMap::new();
env.insert("TEST_VAR".to_string(), "test_value".to_string());
let output = render_test_setup(false, "fixtures", "FixtureE2E", &env);
assert!(output.contains("if (Environment.GetEnvironmentVariable(\"TEST_VAR\") == null)"));
assert!(output.contains("Environment.SetEnvironmentVariable(\"TEST_VAR\", \"test_value\");"));
}
#[test]
fn test_render_csproj_with_extras() {
use crate::core::config::manifest_extras::{ExtraDepSpec, ManifestExtras};
let mut extras = ManifestExtras::default();
extras.dependencies.insert(
"TreeSitter.DotNet".to_string(),
ExtraDepSpec::Simple("1.3.0".to_string()),
);
let output = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Local,
Some(&extras),
);
assert!(
output.contains("<PackageReference Include=\"TreeSitter.DotNet\" Version=\"1.3.0\" />"),
"extras should inject PackageReference, got: {}",
output
);
assert!(
output.contains("<ProjectReference Include=\"../../packages/csharp/MyLib/MyLib.csproj\" />"),
"Local mode should use ProjectReference"
);
}
#[test]
fn test_render_csproj_with_dev_dependencies() {
use crate::core::config::manifest_extras::{ExtraDepSpec, ManifestExtras};
let mut extras = ManifestExtras::default();
extras
.dev_dependencies
.insert("Bogus".to_string(), ExtraDepSpec::Simple("^35.0.0".to_string()));
let output = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Local,
Some(&extras),
);
assert!(
output.contains("<PackageReference Include=\"Bogus\" Version=\"^35.0.0\" />"),
"dev_dependencies should inject as PackageReference, got: {}",
output
);
}
#[test]
fn test_render_csproj_with_both_dependency_buckets() {
use crate::core::config::manifest_extras::{ExtraDepSpec, ManifestExtras};
let mut extras = ManifestExtras::default();
extras.dependencies.insert(
"TreeSitter.DotNet".to_string(),
ExtraDepSpec::Simple("1.3.0".to_string()),
);
extras
.dev_dependencies
.insert("Bogus".to_string(), ExtraDepSpec::Simple("^35.0.0".to_string()));
let output = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Local,
Some(&extras),
);
assert!(output.contains("<PackageReference Include=\"TreeSitter.DotNet\""));
assert!(output.contains("<PackageReference Include=\"Bogus\""));
}
#[test]
fn test_render_csproj_without_extras() {
let output = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Local,
None,
);
assert!(output.contains("Microsoft.NET.Test.Sdk"));
assert!(output.contains("xunit"));
assert!(output.contains("<ProjectReference Include=\"../../packages/csharp/MyLib/MyLib.csproj\" />"));
}
#[test]
fn test_render_csproj_with_empty_extras() {
use crate::core::config::manifest_extras::ManifestExtras;
let extras = ManifestExtras::default();
let output = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Local,
Some(&extras),
);
assert!(output.contains("Microsoft.NET.Test.Sdk"));
assert!(output.contains("<ProjectReference Include=\"../../packages/csharp/MyLib/MyLib.csproj\" />"));
}
#[test]
fn test_render_csproj_registry_mode_ignores_extras() {
use crate::core::config::manifest_extras::{ExtraDepSpec, ManifestExtras};
let mut extras = ManifestExtras::default();
extras.dependencies.insert(
"TreeSitter.DotNet".to_string(),
ExtraDepSpec::Simple("1.3.0".to_string()),
);
let output = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Registry,
Some(&extras),
);
assert!(output.contains("<PackageReference Include=\"MyLib\" Version=\"0.1.0\" />"));
assert!(output.contains("Microsoft.NET.Test.Sdk"));
}
#[test]
fn test_render_csproj_extras_within_item_group() {
use crate::core::config::manifest_extras::{ExtraDepSpec, ManifestExtras};
let mut extras = ManifestExtras::default();
extras.dependencies.insert(
"TreeSitter.DotNet".to_string(),
ExtraDepSpec::Simple("1.3.0".to_string()),
);
extras
.dev_dependencies
.insert("Moq".to_string(), ExtraDepSpec::Simple("4.16.0".to_string()));
let output = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Local,
Some(&extras),
);
let first_item_group_start = output.find("<ItemGroup>").expect("should have ItemGroup");
let first_item_group_end = output.find("</ItemGroup>").expect("should have /ItemGroup");
let first_item_group = &output[first_item_group_start..=first_item_group_end];
assert!(first_item_group.contains("xunit"), "xunit should be in first ItemGroup");
assert!(
first_item_group.contains("TreeSitter.DotNet"),
"extras should be in first ItemGroup"
);
assert!(
first_item_group.contains("Moq"),
"dev_dependencies should also be in first ItemGroup"
);
let second_item_group_start = output[first_item_group_end..]
.find("<ItemGroup>")
.map(|i| i + first_item_group_end)
.expect("should have second ItemGroup");
let second_item_group_end = output[second_item_group_start..]
.find("</ItemGroup>")
.map(|i| i + second_item_group_start + 1)
.unwrap_or(output.len());
let second_item_group = &output[second_item_group_start..second_item_group_end];
assert!(
second_item_group.contains("ProjectReference"),
"second ItemGroup should have ProjectReference"
);
}
#[test]
fn test_render_csproj_idempotent_with_same_extras() {
use crate::core::config::manifest_extras::{ExtraDepSpec, ManifestExtras};
let mut extras = ManifestExtras::default();
extras.dependencies.insert(
"TreeSitter.DotNet".to_string(),
ExtraDepSpec::Simple("1.3.0".to_string()),
);
let output1 = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Local,
Some(&extras),
);
let output2 = render_csproj(
"MyLib",
"../../packages/csharp/MyLib/MyLib.csproj",
"0.1.0",
crate::e2e::config::DependencyMode::Local,
Some(&extras),
);
assert_eq!(output1, output2, "rendering with identical extras should be idempotent");
}
}