chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:33:01 +08:00
commit 34d6a57f38
510 changed files with 163501 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "dotnetshape"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
publish.workspace = true
description = "Inspect .NET project graphs, package references, and MSBuild configuration shape."
keywords.workspace = true
categories.workspace = true
[lints]
workspace = true
[dependencies]
common = { path = "../common", default-features = false }
ignore.workspace = true
lexopt.workspace = true
quick-xml.workspace = true
serde.workspace = true
[dev-dependencies]
assert_cmd.workspace = true
predicates.workspace = true
serde_json.workspace = true
tempfile.workspace = true
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `dotnetshape`.
fn main() {
std::process::exit(dotnetshape::main_entry());
}
+386
View File
@@ -0,0 +1,386 @@
//! 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")
}