387 lines
12 KiB
Rust
387 lines
12 KiB
Rust
//! Integration tests for the `dotnetshape` command.
|
|
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use assert_cmd::Command;
|
|
use predicates::prelude::*;
|
|
use serde_json::Value;
|
|
use tempfile::TempDir;
|
|
|
|
fn cargo_command() -> Command {
|
|
Command::cargo_bin("dotnetshape").expect("binary")
|
|
}
|
|
|
|
fn write_file(path: &Path, contents: &str) {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).expect("parent directory");
|
|
}
|
|
fs::write(path, contents).expect("write fixture");
|
|
}
|
|
|
|
fn json_report(root: &Path) -> Value {
|
|
let output = Command::cargo_bin("dotnetshape")
|
|
.expect("binary")
|
|
.arg("--json")
|
|
.arg(root)
|
|
.output()
|
|
.expect("run dotnetshape");
|
|
assert!(
|
|
output.status.success(),
|
|
"dotnetshape failed\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
serde_json::from_slice(&output.stdout).expect("json output")
|
|
}
|
|
|
|
fn fixture_repo() -> TempDir {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
write_file(
|
|
&temp.path().join("Directory.Build.props"),
|
|
r"<Project>
|
|
<PropertyGroup>
|
|
<Nullable>enable</Nullable>
|
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
|
<BaseOutputPath>artifacts/bin/</BaseOutputPath>
|
|
<AssemblyName>InheritedName</AssemblyName>
|
|
</PropertyGroup>
|
|
</Project>",
|
|
);
|
|
write_file(
|
|
&temp.path().join("Directory.Build.targets"),
|
|
r"<Project>
|
|
<PropertyGroup>
|
|
<OutputType>Exe</OutputType>
|
|
</PropertyGroup>
|
|
</Project>",
|
|
);
|
|
write_file(
|
|
&temp.path().join("Directory.Packages.props"),
|
|
r#"<Project>
|
|
<ItemGroup>
|
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
|
<PackageVersion Update="xunit" Version="2.9.2" />
|
|
</ItemGroup>
|
|
</Project>"#,
|
|
);
|
|
write_file(
|
|
&temp.path().join("repo.sln"),
|
|
"Microsoft Visual Studio Solution File\n",
|
|
);
|
|
write_file(
|
|
&temp.path().join("src").join("App").join("App.csproj"),
|
|
r#"<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<TargetFramework>net8.0</TargetFramework>
|
|
<AssemblyName>SharedAssembly</AssemblyName>
|
|
<OutputType>Library</OutputType>
|
|
<OutputPath>artifacts/shared/</OutputPath>
|
|
<Nullable Condition="'$(Configuration)' == 'Release'">disable</Nullable>
|
|
</PropertyGroup>
|
|
<ItemGroup>
|
|
<ProjectReference Include="../Lib/Lib.csproj" />
|
|
<ProjectReference Include="../Missing/Missing.csproj" />
|
|
<Reference Include="Legacy">
|
|
<HintPath>..\lib\Legacy.dll</HintPath>
|
|
<Private>false</Private>
|
|
</Reference>
|
|
<Reference Include="Implicit">
|
|
<HintPath>..\lib\Implicit.dll</HintPath>
|
|
</Reference>
|
|
<PackageReference Include="Newtonsoft.Json" />
|
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
|
<PackageReference Include="Missing.Version" />
|
|
</ItemGroup>
|
|
<Target Name="ManualCompile">
|
|
<Csc Sources="Program.cs" />
|
|
</Target>
|
|
</Project>"#,
|
|
);
|
|
write_file(
|
|
&temp.path().join("src").join("Lib").join("Lib.csproj"),
|
|
r#"<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
|
|
<AssemblyName>SharedAssembly</AssemblyName>
|
|
<OutputPath>artifacts/shared/</OutputPath>
|
|
</PropertyGroup>
|
|
<ItemGroup>
|
|
<Reference Include="PrivateLegacy">
|
|
<HintPath>..\lib\PrivateLegacy.dll</HintPath>
|
|
<Private>true</Private>
|
|
</Reference>
|
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
|
<PackageReference Include="xunit" />
|
|
</ItemGroup>
|
|
</Project>"#,
|
|
);
|
|
write_file(
|
|
&temp
|
|
.path()
|
|
.join("tests")
|
|
.join("Unit.Tests")
|
|
.join("Unit.Tests.csproj"),
|
|
r#"<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<TargetFramework>net8.0</TargetFramework>
|
|
<IsTestProject>true</IsTestProject>
|
|
</PropertyGroup>
|
|
</Project>"#,
|
|
);
|
|
write_file(
|
|
&temp.path().join("build.ps1"),
|
|
"Write-Host build\ncsc.exe Program.cs\ndotnet build src/App/App.csproj\n",
|
|
);
|
|
temp
|
|
}
|
|
|
|
#[test]
|
|
fn help_lists_shared_and_repo_options() {
|
|
let mut command = cargo_command();
|
|
command
|
|
.arg("--help")
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("--format <FORMAT>"))
|
|
.stdout(predicate::str::contains("--json"))
|
|
.stdout(predicate::str::contains("--toon"))
|
|
.stdout(predicate::str::contains("--max-depth <COUNT>"))
|
|
.stdout(predicate::str::contains("--hidden"))
|
|
.stdout(predicate::str::contains("dotnetshape . --json"));
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::too_many_lines)]
|
|
fn json_reports_static_project_shape_and_diagnostics() {
|
|
let temp = fixture_repo();
|
|
let report = json_report(temp.path());
|
|
|
|
assert_eq!(report["evaluation_mode"], "static_ancestor_merge");
|
|
let projects = report["projects"].as_array().expect("projects");
|
|
assert_eq!(projects.len(), 3);
|
|
|
|
let app = projects
|
|
.iter()
|
|
.find(|project| project["path"] == "src/App/App.csproj")
|
|
.expect("app project");
|
|
assert_eq!(app["sdk"], "Microsoft.NET.Sdk");
|
|
assert_eq!(app["target_frameworks"], serde_json::json!(["net8.0"]));
|
|
assert_eq!(app["assembly_name"], "SharedAssembly");
|
|
assert_eq!(app["output_type"], "Library");
|
|
assert_eq!(app["nullable"], "enable");
|
|
assert_eq!(app["treat_warnings_as_errors"], "true");
|
|
assert_eq!(app["analysis_mode"], "AllEnabledByDefault");
|
|
assert_eq!(app["kind"], "production");
|
|
assert!(
|
|
app["conditioned_properties"]
|
|
.as_array()
|
|
.expect("conditioned properties")
|
|
.iter()
|
|
.any(|property| property["name"] == "Nullable"
|
|
&& property["value"] == "disable"
|
|
&& property["condition"]
|
|
.as_str()
|
|
.is_some_and(|condition| condition.contains("Release")))
|
|
);
|
|
|
|
let references = report["project_references"]
|
|
.as_array()
|
|
.expect("project refs");
|
|
assert!(
|
|
references
|
|
.iter()
|
|
.any(|edge| edge["from"] == "src/App/App.csproj"
|
|
&& edge["to"] == "src/Lib/Lib.csproj"
|
|
&& edge["resolved"] == true)
|
|
);
|
|
assert!(
|
|
references
|
|
.iter()
|
|
.any(|edge| edge["from"] == "src/App/App.csproj"
|
|
&& edge["to"] == "src/Missing/Missing.csproj"
|
|
&& edge["resolved"] == false)
|
|
);
|
|
|
|
let hint_refs = report["reference_hints"].as_array().expect("hint refs");
|
|
assert!(hint_refs.iter().any(|edge| edge["include"] == "Legacy"
|
|
&& edge["hint_path"] == "../lib/Legacy.dll"
|
|
&& edge["private"] == false));
|
|
assert!(
|
|
hint_refs
|
|
.iter()
|
|
.any(|edge| edge["include"] == "Implicit" && edge["private"].is_null())
|
|
);
|
|
assert!(
|
|
hint_refs
|
|
.iter()
|
|
.any(|edge| edge["include"] == "PrivateLegacy" && edge["private"] == true)
|
|
);
|
|
|
|
let packages = report["package_references"].as_array().expect("packages");
|
|
assert!(
|
|
packages
|
|
.iter()
|
|
.any(|package| package["include"] == "Newtonsoft.Json"
|
|
&& package["version"] == "13.0.3"
|
|
&& package["version_source"] == "central")
|
|
);
|
|
assert!(packages.iter().any(|package| package["include"] == "Dapper"
|
|
&& package["version"] == "2.1.66"
|
|
&& package["version_source"] == "inline"));
|
|
assert!(
|
|
packages
|
|
.iter()
|
|
.any(|package| package["include"] == "Missing.Version"
|
|
&& package["version"].is_null()
|
|
&& package["version_source"] == "missing")
|
|
);
|
|
assert!(packages.iter().any(|package| package["include"] == "xunit"
|
|
&& package["version"] == "2.9.2"
|
|
&& package["version_source"] == "central"));
|
|
|
|
assert!(projects.iter().any(|project| {
|
|
project["path"] == "src/Lib/Lib.csproj"
|
|
&& project["kind"] == "test"
|
|
&& project["kind_reasons"]
|
|
.as_array()
|
|
.expect("kind reasons")
|
|
.iter()
|
|
.any(|reason| {
|
|
reason
|
|
.as_str()
|
|
.is_some_and(|value| value.contains("Microsoft.NET.Test.Sdk"))
|
|
})
|
|
}));
|
|
assert!(projects.iter().any(|project| {
|
|
project["path"] == "tests/Unit.Tests/Unit.Tests.csproj"
|
|
&& project["kind"] == "test"
|
|
&& project["kind_reasons"]
|
|
.as_array()
|
|
.expect("kind reasons")
|
|
.iter()
|
|
.any(|reason| {
|
|
reason
|
|
.as_str()
|
|
.is_some_and(|value| value.contains("IsTestProject"))
|
|
})
|
|
}));
|
|
|
|
let diagnostics = report["diagnostics"].as_array().expect("diagnostics");
|
|
assert!(
|
|
diagnostics
|
|
.iter()
|
|
.any(|diagnostic| diagnostic["kind"] == "duplicate_assembly_name")
|
|
);
|
|
assert!(
|
|
diagnostics
|
|
.iter()
|
|
.any(|diagnostic| diagnostic["kind"] == "shared_output_path")
|
|
);
|
|
assert!(
|
|
diagnostics
|
|
.iter()
|
|
.any(|diagnostic| diagnostic["kind"] == "unresolved_project_reference")
|
|
);
|
|
assert!(
|
|
diagnostics
|
|
.iter()
|
|
.any(|diagnostic| diagnostic["kind"] == "missing_package_version")
|
|
);
|
|
|
|
let bypass_hints = report["build_bypass_hints"]
|
|
.as_array()
|
|
.expect("bypass hints");
|
|
assert!(
|
|
bypass_hints
|
|
.iter()
|
|
.any(|hint| hint["kind"] == "csc_task" && hint["path"] == "src/App/App.csproj")
|
|
);
|
|
assert!(
|
|
bypass_hints.iter().any(|hint| hint["kind"] == "csc_exe"
|
|
&& hint["path"] == "build.ps1"
|
|
&& hint["line"] == 2)
|
|
);
|
|
assert!(
|
|
bypass_hints
|
|
.iter()
|
|
.any(|hint| hint["kind"] == "direct_project_build"
|
|
&& hint["path"] == "build.ps1"
|
|
&& hint["line"] == 3)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn max_depth_and_hidden_control_project_discovery() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
write_file(
|
|
&temp.path().join("Root.csproj"),
|
|
r"<Project><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup></Project>",
|
|
);
|
|
write_file(
|
|
&temp.path().join("deep").join("Nested.csproj"),
|
|
r"<Project><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup></Project>",
|
|
);
|
|
write_file(
|
|
&temp.path().join(".hidden").join("Hidden.csproj"),
|
|
r"<Project><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup></Project>",
|
|
);
|
|
|
|
let shallow = json_report_with_args(temp.path(), &["--max-depth", "1"]);
|
|
assert_eq!(shallow["projects"].as_array().expect("projects").len(), 1);
|
|
assert!(
|
|
shallow["projects"]
|
|
.as_array()
|
|
.expect("projects")
|
|
.iter()
|
|
.all(|project| project["path"] != "deep/Nested.csproj"
|
|
&& project["path"] != ".hidden/Hidden.csproj")
|
|
);
|
|
|
|
let hidden = json_report_with_args(temp.path(), &["--hidden"]);
|
|
assert!(
|
|
hidden["projects"]
|
|
.as_array()
|
|
.expect("projects")
|
|
.iter()
|
|
.any(|project| project["path"] == ".hidden/Hidden.csproj")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn text_and_toon_outputs_are_structured() {
|
|
let temp = fixture_repo();
|
|
let mut text = cargo_command();
|
|
text.arg(temp.path())
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("dotnetshape"))
|
|
.stdout(predicate::str::contains("projects:"))
|
|
.stdout(predicate::str::contains("diagnostics:"));
|
|
|
|
let mut toon = cargo_command();
|
|
toon.arg("--toon")
|
|
.arg(temp.path())
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("evaluation_mode"))
|
|
.stdout(predicate::str::contains("projects"));
|
|
}
|
|
|
|
fn json_report_with_args(root: &Path, args: &[&str]) -> Value {
|
|
let mut command = cargo_command();
|
|
command.arg("--json");
|
|
for arg in args {
|
|
command.arg(arg);
|
|
}
|
|
let output = command.arg(root).output().expect("run dotnetshape");
|
|
assert!(
|
|
output.status.success(),
|
|
"dotnetshape failed\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
serde_json::from_slice(&output.stdout).expect("json output")
|
|
}
|