use std::{
cell::RefCell,
fmt,
io::{Cursor, Write},
path::{Path, PathBuf},
rc::Rc,
};
use cargo_semver_checks::{Check, GlobalConfig};
use clap::Parser as _;
use semver::Version;
use crate::Cargo;
#[derive(Debug, Default, Clone)]
struct StaticWriter(Rc<RefCell<Cursor<Vec<u8>>>>);
impl StaticWriter {
#[inline]
#[must_use]
fn new() -> Self {
Self::default()
}
fn try_into_inner(self) -> Result<Vec<u8>, Self> {
match Rc::try_unwrap(self.0) {
Ok(b) => Ok(b.into_inner().into_inner()),
Err(rc) => Err(Self(rc)),
}
}
}
impl Write for StaticWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.borrow_mut().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.borrow_mut().flush()
}
}
#[derive(Debug)]
struct CommandOutput {
stderr: String,
stdout: String,
}
impl fmt::Display for CommandOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "--- stdout ---\n{}", self.stdout)?;
writeln!(f, "--- stderr ---\n{}", self.stderr)?;
Ok(())
}
}
#[derive(Debug)]
struct CommandResult {
result: anyhow::Result<bool>,
output: CommandOutput,
}
impl fmt::Display for CommandResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.result {
Ok(success) => writeln!(f, "success: {success}")?,
Err(e) => writeln!(f, "--- error ---\n{e}")?,
};
write!(f, "{}", self.output)
}
}
fn assert_integration_test(test_name: &str, invocation: &[&str]) {
std::env::remove_var("RUST_BACKTRACE");
std::env::remove_var("CARGO_TERM_VERBOSE");
let stdout = StaticWriter::new();
let stderr = StaticWriter::new();
let Cargo::SemverChecks(arguments) = Cargo::parse_from(invocation);
let mut config = GlobalConfig::new();
config.set_stdout(Box::new(stdout.clone()));
config.set_stderr(Box::new(stderr.clone()));
config.set_color_choice(false);
config.set_log_level(arguments.verbosity.log_level());
let check = Check::from(arguments.check_release);
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../test_outputs/snapshot_tests");
settings.add_filter(r"\[\s*[\d\.]+s\]", "[TIME]");
settings.add_filter(r"\d+ checks", "[TOTAL] checks");
settings.add_filter(r"\d+ pass", "[PASS] pass");
settings.add_filter(r"\d+ skip", "[SKIP] skip");
let repo_root = get_root_path();
settings.add_filter(®ex::escape(&repo_root.to_string_lossy()), "[ROOT]");
settings.add_filter(" Blocking waiting for file lock on [^\n]+\n", "");
settings.add_filter(
r"v\d+\.\d+\.\d+(-[\w\.-]+)?/src/lints",
"[VERSION]/src/lints",
);
let _grd = settings.bind_to_scope();
insta::assert_ron_snapshot!(format!("{test_name}-input"), check);
let result = check.check_release(&mut config);
drop(config);
let stdout = stdout
.try_into_inner()
.expect("failed to get unique reference to stdout");
let stderr = stderr
.try_into_inner()
.expect("failed to get unique reference to stderr");
let stdout = String::from_utf8(stdout).expect("failed to convert to UTF-8");
let stderr = String::from_utf8(stderr).expect("failed to convert to UTF-8");
let result = CommandResult {
result: result.map(|report| report.success()),
output: CommandOutput { stderr, stdout },
};
insta::assert_snapshot!(format!("{test_name}-output"), result);
}
fn get_root_path() -> PathBuf {
let canonicalized = Path::new(file!())
.canonicalize()
.expect("canonicalization failed");
let repo_root = canonicalized
.parent()
.and_then(Path::parent)
.expect("getting repo root failed");
repo_root.to_owned()
}
#[test]
fn workspace_no_lib_targets_error() {
assert_integration_test(
"workspace_no_lib_targets",
&[
"cargo",
"semver-checks",
"--manifest-path",
"test_crates/manifest_tests/no_lib_targets/new",
"--baseline-root",
"test_crates/manifest_tests/no_lib_targets/old",
"--workspace",
],
);
}
#[test]
fn workspace_all_publish_false() {
assert_integration_test(
"workspace_all_publish_false",
&[
"cargo",
"semver-checks",
"--manifest-path",
"test_crates/manifest_tests/workspace_all_publish_false/new",
"--baseline-root",
"test_crates/manifest_tests/workspace_all_publish_false/old",
"--workspace",
],
);
}
#[test]
fn workspace_publish_false_explicit() {
assert_integration_test(
"workspace_publish_false_explicit",
&[
"cargo",
"semver-checks",
"--manifest-path",
"test_crates/manifest_tests/workspace_all_publish_false/new",
"--baseline-root",
"test_crates/manifest_tests/workspace_all_publish_false/old",
"--package",
"a",
],
)
}
#[test]
fn workspace_publish_false_workspace_flag() {
assert_integration_test(
"workspace_publish_false_workspace_flag",
&[
"cargo",
"semver-checks",
"--manifest-path",
"test_crates/manifest_tests/workspace_all_publish_false/new",
"--baseline-root",
"test_crates/manifest_tests/workspace_all_publish_false/old",
"--workspace",
"--package",
"a",
"--verbose",
],
)
}
#[test]
fn workspace_baseline_compile_error() {
if rustc_version::version().map_or(true, |version| version < Version::new(1, 78, 0)) {
eprintln!(
"Skipping this test as `cargo doc` output is different in earlier versions.
Consider rerunning with cargo >= 1.78"
);
return;
}
assert_integration_test(
"workspace_baseline_compile_error",
&[
"cargo",
"semver-checks",
"--baseline-root",
"test_crates/manifest_tests/workspace_baseline_compile_error/old",
"--manifest-path",
"test_crates/manifest_tests/workspace_baseline_compile_error/new",
"--workspace",
],
);
}
#[test]
fn workspace_baseline_conditional_compile_error() {
if rustc_version::version().map_or(true, |version| version < Version::new(1, 78, 0)) {
eprintln!(
"Skipping this test as `cargo doc` output is different in earlier versions.
Consider rerunning with cargo >= 1.78"
);
return;
}
assert_integration_test(
"workspace_baseline_conditional_compile_error",
&[
"cargo",
"semver-checks",
"--baseline-root",
"test_crates/manifest_tests/workspace_baseline_conditional_compile_error/old",
"--manifest-path",
"test_crates/manifest_tests/workspace_baseline_conditional_compile_error/new",
"--workspace",
"--only-explicit-features", ],
);
}
#[test]
fn multiple_ambiguous_package_name_definitions() {
assert_integration_test(
"multiple_ambiguous_package_name_definitions",
&[
"cargo",
"semver-checks",
"--baseline-root",
"test_crates/manifest_tests/multiple_ambiguous_package_name_definitions",
"--manifest-path",
"test_crates/manifest_tests/multiple_ambiguous_package_name_definitions",
],
);
}
#[test]
fn semver_trick_self_referential() {
assert_integration_test(
"semver_trick_self_referential",
&[
"cargo",
"semver-checks",
"--baseline-root",
"test_crates/semver_trick_self_referential/old/",
"--manifest-path",
"test_crates/semver_trick_self_referential/new/",
],
);
}
fn recurse_list_files(path: impl AsRef<Path>) -> Vec<std::fs::DirEntry> {
let mut buf = vec![];
let entries = std::fs::read_dir(path).expect("failed to read the requested path");
for entry in entries {
let entry = entry.expect("failed to iterate due to intermittent IO errors");
let meta = entry.metadata().expect("failed to read file metadata");
if meta.is_dir() {
let mut subdir = recurse_list_files(entry.path());
buf.append(&mut subdir);
}
if meta.is_file() {
buf.push(entry);
}
}
buf
}
#[test]
fn no_new_snapshots() {
let files = recurse_list_files("test_outputs/");
let new_snaps = files
.into_iter()
.map(|f| f.path())
.filter(|f| {
if let Some(name) = f.file_name().unwrap().to_str() {
return name.ends_with("snap.new");
}
false
})
.collect::<Vec<_>>();
assert_eq!(
new_snaps,
Vec::<PathBuf>::new(),
"`.snap.new` files exit, but should not. Did you\n\
- forget to run `cargo insta review` or\n\
- forget to move the `.snap.new` file to `.snap` after verifying \
the content is exactly as expected?"
);
}