chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "asmref"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
readme.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Inspect managed assembly references with AI-friendly output."
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common", default-features = false }
|
||||
managed = { path = "../managed" }
|
||||
lexopt.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd.workspace = true
|
||||
predicates.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -0,0 +1,598 @@
|
||||
//! The `asmref` command inspects managed assembly references.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Write as _;
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::{
|
||||
CliError, CommonArgs, ExitCode, InputFormat, RenderMode, map_result_count, parse_color_choice,
|
||||
parse_format_choice, parse_input_format, print_json, print_quick_help_error, print_structured,
|
||||
read_existing_stdin_path_records, should_read_stdin, write_stdout,
|
||||
};
|
||||
use lexopt::prelude::{Long, Short, Value as ArgValue};
|
||||
use managed::{
|
||||
DependencyDiagnosisReport, DependencyReferenceDiagnostic, DiagnoseQuery, ReferenceQuery,
|
||||
ResolutionStatus, diagnose_dependencies, inspect_references,
|
||||
};
|
||||
|
||||
const HELP: &str = "\
|
||||
Inspect managed assembly references and simple resolution status.
|
||||
|
||||
Usage:
|
||||
asmref [OPTIONS] [ASSEMBLY...]
|
||||
asmref diagnose [OPTIONS] [ASSEMBLY...]
|
||||
|
||||
Options:
|
||||
--format <FORMAT> Structured output format: text, json, toon
|
||||
--json Shortcut for --format json
|
||||
--toon Shortcut for --format toon
|
||||
--input-format <FORMAT> Override stdin parsing mode: auto, lines, jsonl
|
||||
--color <WHEN> Control ANSI color output: auto, never
|
||||
--quiet Suppress non-essential status output
|
||||
--resolve-dir <PATH> Additional directory to search for references
|
||||
-h, --help Show this help text
|
||||
-V, --version Show the command version
|
||||
|
||||
Examples:
|
||||
asmref .\\fixtures\\managed\\bin\\GameAssembly.dll --resolve-dir .\\fixtures\\managed\\bin
|
||||
asmref diagnose .\\Plugins\\Example.Plugin.dll --resolve-dir .\\Libraries --resolve-dir .\\Managed --format toon
|
||||
'C:\\game\\Managed\\Assembly-CSharp.dll' | asmref --input-format lines --resolve-dir C:\\game\\Managed
|
||||
asmref .\\fixtures\\managed\\bin\\GameAssembly.dll --json | ConvertFrom-Json
|
||||
";
|
||||
|
||||
const DIAGNOSE_HELP: &str = "\
|
||||
Diagnose managed assembly dependency closure resolution.
|
||||
|
||||
Usage:
|
||||
asmref diagnose [OPTIONS] [ASSEMBLY...]
|
||||
|
||||
Options:
|
||||
--format <FORMAT> Structured output format: text, json, toon
|
||||
--json Shortcut for --format json
|
||||
--toon Shortcut for --format toon
|
||||
--input-format <FORMAT> Override stdin parsing mode: auto, lines, jsonl
|
||||
--color <WHEN> Control ANSI color output: auto, never
|
||||
--quiet Suppress non-essential status output
|
||||
--resolve-dir <PATH> Additional directory to search for dependencies
|
||||
--test-only-pattern <REGEX> Extra case-insensitive test-only name/path pattern
|
||||
--no-default-test-patterns Disable built-in test-only markers
|
||||
-h, --help Show this help text
|
||||
-V, --version Show the command version
|
||||
|
||||
Examples:
|
||||
asmref diagnose .\\Plugins\\Example.Plugin.dll --resolve-dir .\\Libraries --resolve-dir .\\Managed --format toon
|
||||
asmref diagnose .\\RootPlugin.dll --test-only-pattern Project.Tests --json
|
||||
asmref diagnose .\\RootPlugin.dll --no-default-test-patterns --toon
|
||||
";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Cli {
|
||||
mode: CommandMode,
|
||||
common: CommonArgs,
|
||||
assembly_paths: Vec<PathBuf>,
|
||||
query: ReferenceQuery,
|
||||
diagnose_query: DiagnoseQuery,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CommandMode {
|
||||
Inspect,
|
||||
Diagnose,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ParseOutcome {
|
||||
Help(CommandMode),
|
||||
Version,
|
||||
Run,
|
||||
}
|
||||
|
||||
/// Parses CLI arguments and returns a process exit code.
|
||||
#[must_use]
|
||||
pub fn main_entry() -> i32 {
|
||||
match parse_cli_from(std::env::args_os()) {
|
||||
Ok((ParseOutcome::Help(CommandMode::Inspect), _)) => {
|
||||
print!("{HELP}");
|
||||
ExitCode::Success.as_i32()
|
||||
}
|
||||
Ok((ParseOutcome::Help(CommandMode::Diagnose), _)) => {
|
||||
print!("{DIAGNOSE_HELP}");
|
||||
ExitCode::Success.as_i32()
|
||||
}
|
||||
Ok((ParseOutcome::Version, _)) => {
|
||||
println!("asmref {}", env!("CARGO_PKG_VERSION"));
|
||||
ExitCode::Success.as_i32()
|
||||
}
|
||||
Ok((ParseOutcome::Run, cli)) => match run(&cli) {
|
||||
Ok(code) => code.as_i32(),
|
||||
Err(error) => {
|
||||
print_quick_help_error(&error, help_for_mode(cli.mode));
|
||||
error.exit_code().as_i32()
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
print_quick_help_error(&error, HELP);
|
||||
error.exit_code().as_i32()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn help_for_mode(mode: CommandMode) -> &'static str {
|
||||
match mode {
|
||||
CommandMode::Inspect => HELP,
|
||||
CommandMode::Diagnose => DIAGNOSE_HELP,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cli_from<I, T>(args: I) -> Result<(ParseOutcome, Cli), CliError>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString>,
|
||||
{
|
||||
let mut parser = lexopt::Parser::from_iter(args);
|
||||
let mut cli = Cli {
|
||||
mode: CommandMode::Inspect,
|
||||
common: CommonArgs::default(),
|
||||
assembly_paths: Vec::new(),
|
||||
query: ReferenceQuery::default(),
|
||||
diagnose_query: DiagnoseQuery::default(),
|
||||
};
|
||||
|
||||
while let Some(argument) = parser
|
||||
.next()
|
||||
.map_err(|error| CliError::usage(error.to_string()))?
|
||||
{
|
||||
match argument {
|
||||
Long("help") | Short('h') => return Ok((ParseOutcome::Help(cli.mode), cli)),
|
||||
Long("version") | Short('V') => return Ok((ParseOutcome::Version, cli)),
|
||||
Long("json") => cli.common.set_render_mode(RenderMode::Json),
|
||||
Long("toon") => cli.common.set_render_mode(RenderMode::Toon),
|
||||
Long("format") => {
|
||||
let value = parser_value_string(&mut parser, "--format")?;
|
||||
cli.common.set_render_mode(parse_format_choice(&value)?);
|
||||
}
|
||||
Long("input-format") => {
|
||||
cli.common.input_format =
|
||||
parse_input_format(&parser_value_string(&mut parser, "--input-format")?)?;
|
||||
}
|
||||
Long("color") => {
|
||||
cli.common.color =
|
||||
parse_color_choice(&parser_value_string(&mut parser, "--color")?)?;
|
||||
}
|
||||
Long("quiet") => cli.common.quiet = true,
|
||||
Long("resolve-dir") => cli.push_resolve_dir(PathBuf::from(parser_value_string(
|
||||
&mut parser,
|
||||
"--resolve-dir",
|
||||
)?)),
|
||||
Long("test-only-pattern") if cli.mode == CommandMode::Diagnose => cli
|
||||
.diagnose_query
|
||||
.test_only_patterns
|
||||
.push(parser_value_string(&mut parser, "--test-only-pattern")?),
|
||||
Long("no-default-test-patterns") if cli.mode == CommandMode::Diagnose => {
|
||||
cli.diagnose_query.use_default_test_patterns = false;
|
||||
}
|
||||
ArgValue(value)
|
||||
if cli.mode == CommandMode::Inspect
|
||||
&& cli.assembly_paths.is_empty()
|
||||
&& value.to_string_lossy() == "diagnose" =>
|
||||
{
|
||||
cli.mode = CommandMode::Diagnose;
|
||||
}
|
||||
ArgValue(value) => cli.assembly_paths.push(PathBuf::from(value)),
|
||||
_ => {
|
||||
return Err(CliError::usage(
|
||||
"unsupported argument; use --help to see available options",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((ParseOutcome::Run, cli))
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
fn push_resolve_dir(&mut self, path: PathBuf) {
|
||||
self.query.resolve_dirs.push(path.clone());
|
||||
self.diagnose_query.resolve_dirs.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn parser_value_string(parser: &mut lexopt::Parser, flag: &str) -> Result<String, CliError> {
|
||||
let value = parser
|
||||
.value()
|
||||
.map_err(|error| CliError::usage(error.to_string()))?;
|
||||
value.into_string().map_err(|invalid| {
|
||||
CliError::usage(format!(
|
||||
"{flag} expects UTF-8 text, got '{}'",
|
||||
invalid.to_string_lossy()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn run(cli: &Cli) -> Result<ExitCode, CliError> {
|
||||
let paths = collect_paths(cli)?;
|
||||
if paths.is_empty() {
|
||||
return Err(CliError::usage(
|
||||
"provide at least one assembly path or pipe assembly paths into stdin",
|
||||
));
|
||||
}
|
||||
|
||||
match cli.mode {
|
||||
CommandMode::Inspect => run_inspect(cli, &paths),
|
||||
CommandMode::Diagnose => run_diagnose(cli, &paths),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_inspect(cli: &Cli, paths: &[PathBuf]) -> Result<ExitCode, CliError> {
|
||||
let mut reports = inspect_references(paths, &cli.query)
|
||||
.map_err(|error| CliError::runtime(error.to_string()))?;
|
||||
let exit_code = map_result_count(reports.len());
|
||||
|
||||
match cli.common.render_mode() {
|
||||
RenderMode::Json => {
|
||||
if reports.len() == 1 {
|
||||
print_json(&reports.remove(0))?;
|
||||
} else {
|
||||
print_json(&reports)?;
|
||||
}
|
||||
}
|
||||
RenderMode::Toon => {
|
||||
if reports.len() == 1 {
|
||||
print_structured(&reports.remove(0), RenderMode::Toon)?;
|
||||
} else {
|
||||
print_structured(&reports, RenderMode::Toon)?;
|
||||
}
|
||||
}
|
||||
RenderMode::Text => {
|
||||
for report in &reports {
|
||||
if report.references.is_empty() {
|
||||
println!(
|
||||
"{} references=0 path={}",
|
||||
report.assembly.assembly_name,
|
||||
report.assembly.path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for reference in &report.references {
|
||||
println!(
|
||||
"{} name={} resolved={} path={}",
|
||||
report.assembly.assembly_name,
|
||||
reference.name,
|
||||
reference.resolved,
|
||||
reference
|
||||
.resolved_path
|
||||
.as_deref()
|
||||
.map_or_else(|| "-".to_string(), |value| value.display().to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
||||
fn run_diagnose(cli: &Cli, paths: &[PathBuf]) -> Result<ExitCode, CliError> {
|
||||
let report = diagnose_dependencies(paths, &cli.diagnose_query)
|
||||
.map_err(|error| CliError::runtime(error.to_string()))?;
|
||||
let exit_code = if report.summary.error_count == 0 {
|
||||
ExitCode::Success
|
||||
} else {
|
||||
ExitCode::NoResults
|
||||
};
|
||||
|
||||
match cli.common.render_mode() {
|
||||
RenderMode::Json => print_json(&report)?,
|
||||
RenderMode::Toon => print_structured(&report, RenderMode::Toon)?,
|
||||
RenderMode::Text => write_stdout(&render_diagnose_text(&report))?,
|
||||
}
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
||||
fn render_diagnose_text(report: &DependencyDiagnosisReport) -> String {
|
||||
let mut output = String::new();
|
||||
let summary = &report.summary;
|
||||
let _ = writeln!(
|
||||
&mut output,
|
||||
"summary roots={} assemblies={} references={} resolved={} missing={} conflicts={} test_only={} errors={} warnings={} infos={}",
|
||||
summary.root_count,
|
||||
summary.assembly_count,
|
||||
summary.reference_count,
|
||||
summary.resolved_count,
|
||||
summary.missing_count,
|
||||
summary.conflict_count,
|
||||
summary.test_only_count,
|
||||
summary.error_count,
|
||||
summary.warning_count,
|
||||
summary.info_count
|
||||
);
|
||||
|
||||
output.push_str("missing\n");
|
||||
for reference in report
|
||||
.references
|
||||
.iter()
|
||||
.filter(|item| item.resolution_status == ResolutionStatus::Missing)
|
||||
{
|
||||
let _ = writeln!(
|
||||
&mut output,
|
||||
" {} -> {} requested={}",
|
||||
reference.source_assembly, reference.reference_name, reference.requested_version
|
||||
);
|
||||
}
|
||||
|
||||
output.push_str("conflicts\n");
|
||||
for conflict in &report.conflicts {
|
||||
let _ = writeln!(
|
||||
&mut output,
|
||||
" {} reason={} candidates={}",
|
||||
conflict.reference_name,
|
||||
conflict.reason,
|
||||
conflict.candidates.len()
|
||||
);
|
||||
}
|
||||
|
||||
output.push_str("winners\n");
|
||||
for winner in &report.winners {
|
||||
let _ = writeln!(
|
||||
&mut output,
|
||||
" {} requested={} winner={} version={} reason={:?}",
|
||||
winner.reference_name,
|
||||
winner.requested_version,
|
||||
winner.winner.assembly.path.display(),
|
||||
winner
|
||||
.winner
|
||||
.assembly
|
||||
.assembly_version
|
||||
.as_deref()
|
||||
.unwrap_or("-"),
|
||||
winner.reason
|
||||
);
|
||||
}
|
||||
|
||||
output.push_str("test_only\n");
|
||||
for candidate in &report.test_only {
|
||||
let _ = writeln!(
|
||||
&mut output,
|
||||
" {} version={} path={}",
|
||||
candidate.assembly.assembly_name,
|
||||
candidate
|
||||
.assembly
|
||||
.assembly_version
|
||||
.as_deref()
|
||||
.unwrap_or("-"),
|
||||
candidate.assembly.path.display()
|
||||
);
|
||||
}
|
||||
|
||||
output.push_str("risks\n");
|
||||
for risk in &report.risks {
|
||||
let _ = writeln!(
|
||||
&mut output,
|
||||
" {} {} source={} reference={} path={} message={}",
|
||||
risk.severity,
|
||||
risk.kind,
|
||||
risk.source_assembly.as_deref().unwrap_or("-"),
|
||||
risk.reference_name.as_deref().unwrap_or("-"),
|
||||
risk.path
|
||||
.as_deref()
|
||||
.map_or_else(|| "-".to_string(), |path| path.display().to_string()),
|
||||
risk.message
|
||||
);
|
||||
}
|
||||
|
||||
output.push_str("notable_refs\n");
|
||||
for reference in &report.notable_refs {
|
||||
output.push_str(&render_notable_reference(reference));
|
||||
}
|
||||
|
||||
if !report.scan_warnings.is_empty() {
|
||||
output.push_str("scan_warnings\n");
|
||||
for warning in &report.scan_warnings {
|
||||
let _ = writeln!(&mut output, " {warning}");
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn render_notable_reference(reference: &DependencyReferenceDiagnostic) -> String {
|
||||
format!(
|
||||
" {} -> {} requested={} status={:?} winner={}\n",
|
||||
reference.source_assembly,
|
||||
reference.reference_name,
|
||||
reference.requested_version,
|
||||
reference.resolution_status,
|
||||
reference.winner.as_ref().map_or_else(
|
||||
|| "-".to_string(),
|
||||
|candidate| candidate.assembly.path.display().to_string()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn collect_paths(cli: &Cli) -> Result<Vec<PathBuf>, CliError> {
|
||||
if should_read_stdin(
|
||||
!cli.assembly_paths.is_empty(),
|
||||
cli.common.stdin_is_terminal(),
|
||||
) {
|
||||
let mut buffer = String::new();
|
||||
io::stdin()
|
||||
.read_to_string(&mut buffer)
|
||||
.map_err(|error| CliError::runtime(format!("failed to read stdin: {error}")))?;
|
||||
if let Some(parsed) =
|
||||
read_existing_stdin_path_records(&buffer, cli.common.input_format, "asmref")?
|
||||
{
|
||||
if !parsed.is_empty() {
|
||||
return Ok(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
common::expand_input_patterns(&cli.assembly_paths, "asmref")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn parse_paths_from_string(
|
||||
buffer: &str,
|
||||
input_format: InputFormat,
|
||||
) -> Result<Vec<PathBuf>, CliError> {
|
||||
read_existing_stdin_path_records(buffer, input_format, "asmref")?
|
||||
.map_or_else(|| Ok(Vec::new()), Ok)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::{ColorChoice, InputFormat};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn cli() -> Cli {
|
||||
Cli {
|
||||
mode: CommandMode::Inspect,
|
||||
common: CommonArgs {
|
||||
json: false,
|
||||
format: None,
|
||||
input_format: InputFormat::Auto,
|
||||
color: ColorChoice::Never,
|
||||
quiet: false,
|
||||
},
|
||||
assembly_paths: Vec::new(),
|
||||
query: ReferenceQuery::default(),
|
||||
diagnose_query: DiagnoseQuery::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn common_args(json: bool, input_format: InputFormat) -> CommonArgs {
|
||||
CommonArgs {
|
||||
json,
|
||||
format: None,
|
||||
input_format,
|
||||
color: ColorChoice::Never,
|
||||
quiet: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_root() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.canonicalize()
|
||||
.expect("workspace root")
|
||||
}
|
||||
|
||||
fn fixture_assembly() -> PathBuf {
|
||||
workspace_root()
|
||||
.join("fixtures")
|
||||
.join("managed")
|
||||
.join("bin")
|
||||
.join("GameAssembly.dll")
|
||||
}
|
||||
|
||||
fn fixture_dir() -> PathBuf {
|
||||
workspace_root()
|
||||
.join("fixtures")
|
||||
.join("managed")
|
||||
.join("bin")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_paths_reads_lines_and_jsonl_paths() {
|
||||
let first = fixture_assembly();
|
||||
let second = workspace_root().join("Cargo.toml");
|
||||
assert_eq!(
|
||||
parse_paths_from_string(
|
||||
&format!("{}\n{}\n", first.display(), second.display()),
|
||||
InputFormat::Lines
|
||||
)
|
||||
.expect("line paths"),
|
||||
vec![second, first.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_paths_from_string(
|
||||
&format!(
|
||||
"{{\"path\":{}}}\n",
|
||||
serde_json::to_string(&first.display().to_string()).expect("json path")
|
||||
),
|
||||
InputFormat::Jsonl
|
||||
)
|
||||
.expect("jsonl paths"),
|
||||
vec![first]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_requires_at_least_one_path() {
|
||||
let error = run(&cli()).expect_err("missing paths should fail");
|
||||
assert!(matches!(
|
||||
error,
|
||||
CliError::Usage(message) if message.contains("provide at least one assembly path")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_cli_collects_paths_and_resolve_dirs() {
|
||||
let (_, parsed) = parse_cli_from([
|
||||
"asmref",
|
||||
"--resolve-dir",
|
||||
"managed",
|
||||
"--json",
|
||||
"fixture.dll",
|
||||
])
|
||||
.expect("cli parse");
|
||||
|
||||
assert!(parsed.common.json);
|
||||
assert_eq!(parsed.mode, CommandMode::Inspect);
|
||||
assert_eq!(parsed.assembly_paths, vec![PathBuf::from("fixture.dll")]);
|
||||
assert_eq!(parsed.query.resolve_dirs, vec![PathBuf::from("managed")]);
|
||||
assert_eq!(
|
||||
parsed.diagnose_query.resolve_dirs,
|
||||
vec![PathBuf::from("managed")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_cli_collects_diagnose_options() {
|
||||
let (_, parsed) = parse_cli_from([
|
||||
"asmref",
|
||||
"diagnose",
|
||||
"--resolve-dir",
|
||||
"managed",
|
||||
"--test-only-pattern",
|
||||
"Project.Tests",
|
||||
"--no-default-test-patterns",
|
||||
"fixture.dll",
|
||||
])
|
||||
.expect("cli parse");
|
||||
|
||||
assert_eq!(parsed.mode, CommandMode::Diagnose);
|
||||
assert_eq!(parsed.assembly_paths, vec![PathBuf::from("fixture.dll")]);
|
||||
assert_eq!(
|
||||
parsed.diagnose_query.resolve_dirs,
|
||||
vec![PathBuf::from("managed")]
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.diagnose_query.test_only_patterns,
|
||||
vec!["Project.Tests"]
|
||||
);
|
||||
assert!(!parsed.diagnose_query.use_default_test_patterns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_emits_success_for_fixture_reference_report() {
|
||||
let exit_code = run(&Cli {
|
||||
common: common_args(false, InputFormat::Auto),
|
||||
assembly_paths: vec![fixture_assembly()],
|
||||
mode: CommandMode::Inspect,
|
||||
query: ReferenceQuery {
|
||||
resolve_dirs: vec![fixture_dir()],
|
||||
},
|
||||
diagnose_query: DiagnoseQuery::default(),
|
||||
})
|
||||
.expect("fixture reference run");
|
||||
|
||||
assert_eq!(exit_code, ExitCode::Success);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `asmref`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(asmref::main_entry());
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//! Integration tests for the `asmref` command.
|
||||
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn cargo_command() -> Command {
|
||||
Command::cargo_bin("asmref").expect("binary")
|
||||
}
|
||||
|
||||
fn workspace_root() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.canonicalize()
|
||||
.expect("workspace root")
|
||||
}
|
||||
|
||||
fn managed_fixture_dir() -> PathBuf {
|
||||
workspace_root()
|
||||
.join("fixtures")
|
||||
.join("managed")
|
||||
.join("bin")
|
||||
}
|
||||
|
||||
fn fixture_assembly() -> PathBuf {
|
||||
managed_fixture_dir().join("GameAssembly.dll")
|
||||
}
|
||||
|
||||
fn diagnose_fixture_root() -> PathBuf {
|
||||
workspace_root()
|
||||
.join("fixtures")
|
||||
.join("managed")
|
||||
.join("diagnose-bin")
|
||||
.join("root")
|
||||
.join("RootPlugin.dll")
|
||||
}
|
||||
|
||||
fn diagnose_server_a_dir() -> PathBuf {
|
||||
workspace_root()
|
||||
.join("fixtures")
|
||||
.join("managed")
|
||||
.join("diagnose-bin")
|
||||
.join("server-a")
|
||||
}
|
||||
|
||||
fn diagnose_server_b_dir() -> PathBuf {
|
||||
workspace_root()
|
||||
.join("fixtures")
|
||||
.join("managed")
|
||||
.join("diagnose-bin")
|
||||
.join("server-b")
|
||||
}
|
||||
|
||||
fn framework_reference_free_assembly() -> PathBuf {
|
||||
PathBuf::from(r"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_includes_examples_and_resolve_dir_usage() {
|
||||
let mut command = cargo_command();
|
||||
command
|
||||
.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("--resolve-dir"))
|
||||
.stdout(predicate::str::contains("ConvertFrom-Json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emits_reference_resolution_as_json() {
|
||||
let fixture_dir = managed_fixture_dir();
|
||||
let mut command = cargo_command();
|
||||
let output = command
|
||||
.arg(fixture_assembly())
|
||||
.arg("--resolve-dir")
|
||||
.arg(&fixture_dir)
|
||||
.arg("--json")
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
let payload = serde_json::from_slice::<Value>(&output).expect("json payload");
|
||||
let refs = payload["references"].as_array().expect("references array");
|
||||
assert!(
|
||||
refs.iter().any(|entry| {
|
||||
entry["name"] == "FixtureSupport"
|
||||
&& entry["resolved"] == true
|
||||
&& entry["resolved_path"]
|
||||
.as_str()
|
||||
.is_some_and(|value: &str| value.ends_with("FixtureSupport.dll"))
|
||||
}),
|
||||
"expected FixtureSupport reference to resolve"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_mode_reports_zero_reference_empty_state() {
|
||||
let mut command = cargo_command();
|
||||
command
|
||||
.arg(framework_reference_free_assembly())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("references=0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_path_input_from_powershell_pipeline() {
|
||||
let binary = assert_cmd::cargo::cargo_bin("asmref");
|
||||
let input = fixture_assembly();
|
||||
let fixture_dir = managed_fixture_dir();
|
||||
let script = format!(
|
||||
"'{}' | & '{}' --input-format lines --resolve-dir '{}' --json",
|
||||
input.display(),
|
||||
binary.display(),
|
||||
fixture_dir.display()
|
||||
);
|
||||
|
||||
let mut command = Command::new("pwsh");
|
||||
command
|
||||
.arg("-NoProfile")
|
||||
.arg("-Command")
|
||||
.arg(script)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"name\":\"FixtureSupport\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnose_help_mentions_closure_options() {
|
||||
let mut command = cargo_command();
|
||||
command
|
||||
.args(["diagnose", "--help"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("asmref diagnose"))
|
||||
.stdout(predicate::str::contains("--resolve-dir"))
|
||||
.stdout(predicate::str::contains("--test-only-pattern"))
|
||||
.stdout(predicate::str::contains("--no-default-test-patterns"))
|
||||
.stdout(predicate::str::contains("--format <FORMAT>"))
|
||||
.stdout(predicate::str::contains("--toon"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnose_errors_show_diagnose_usage() {
|
||||
let mut command = cargo_command();
|
||||
command
|
||||
.arg("diagnose")
|
||||
.assert()
|
||||
.code(2)
|
||||
.stderr(predicate::str::contains(
|
||||
"asmref diagnose [OPTIONS] [ASSEMBLY...]",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnose_reports_missing_conflicts_winners_and_risks() {
|
||||
let mut command = cargo_command();
|
||||
let output = command
|
||||
.arg("diagnose")
|
||||
.arg(diagnose_fixture_root())
|
||||
.arg("--resolve-dir")
|
||||
.arg(diagnose_server_a_dir())
|
||||
.arg("--resolve-dir")
|
||||
.arg(diagnose_server_b_dir())
|
||||
.arg("--json")
|
||||
.assert()
|
||||
.code(1)
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
let payload = serde_json::from_slice::<Value>(&output).expect("json payload");
|
||||
|
||||
assert_eq!(payload["summary"]["root_count"], 1);
|
||||
assert!(
|
||||
payload["summary"]["error_count"]
|
||||
.as_u64()
|
||||
.is_some_and(|count| count > 0)
|
||||
);
|
||||
|
||||
let references = payload["references"].as_array().expect("references array");
|
||||
assert!(references.iter().any(|entry| {
|
||||
entry["reference_name"] == "MissingOnly" && entry["resolution_status"] == "missing"
|
||||
}));
|
||||
|
||||
let candidates = payload["candidates"].as_array().expect("candidates array");
|
||||
assert!(candidates.iter().any(|entry| {
|
||||
entry["assembly"]["assembly_name"] == "RuntimeDependency"
|
||||
&& entry["assembly"]["assembly_version"] == "2.0.0.0"
|
||||
}));
|
||||
|
||||
let winners = payload["winners"].as_array().expect("winners array");
|
||||
assert!(winners.iter().any(|entry| {
|
||||
entry["reference_name"] == "0Harmony"
|
||||
&& entry["winner"]["assembly"]["assembly_version"] == "2.2.2.0"
|
||||
}));
|
||||
|
||||
let conflicts = payload["conflicts"].as_array().expect("conflicts array");
|
||||
assert!(
|
||||
conflicts
|
||||
.iter()
|
||||
.any(|entry| entry["reference_name"] == "RuntimeDependency")
|
||||
);
|
||||
|
||||
let test_only = payload["test_only"].as_array().expect("test_only array");
|
||||
assert!(
|
||||
test_only
|
||||
.iter()
|
||||
.any(|entry| entry["assembly"]["assembly_name"] == "TestOnlySupport")
|
||||
);
|
||||
|
||||
let risks = payload["risks"].as_array().expect("risks array");
|
||||
for expected in [
|
||||
"missing_reference",
|
||||
"version_mismatch",
|
||||
"test_only_dependency",
|
||||
"missing_method",
|
||||
"missing_type",
|
||||
] {
|
||||
assert!(
|
||||
risks.iter().any(|entry| entry["kind"] == expected),
|
||||
"expected risk kind {expected} in {risks:#?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user