#![allow(clippy::large_futures)]
#![allow(clippy::disallowed_methods)]
use std::ffi::OsString;
use std::path::Path;
use monochange_test_helpers::git;
use rstest::rstest;
use serde_json::Value;
mod test_support;
use test_support::assert_readable_json_snapshot;
use test_support::copy_directory;
use test_support::current_test_name;
use test_support::fixture_path;
use test_support::monochange_command;
use test_support::setup_scenario_workspace;
use test_support::snapshot_settings;
#[rstest]
#[case::detects_package_changes(
"affected/single-package",
&["--changed-paths", "crates/core/src/lib.rs"]
)]
#[case::reports_not_required_for_non_package_changes(
"affected/single-package",
&["--changed-paths", "docs/readme.md"]
)]
#[case::respects_package_ignored_paths(
"affected/ignored-paths",
&["--changed-paths", "crates/core/tests/smoke.rs"]
)]
#[case::ignores_configured_package_changelog_paths(
"affected/changelog-paths",
&["--changed-paths", "crates/core/changelog.md"]
)]
#[case::respects_package_additional_paths(
"affected/additional-paths",
&["--changed-paths", "Cargo.lock"]
)]
#[case::skips_when_allowed_label_is_present(
"affected/skip-label",
&[
"--changed-paths",
"crates/core/src/lib.rs",
"--label",
"no-changeset-required",
]
)]
#[case::accepts_changesets_covering_changed_packages(
"affected/single-package-with-changeset",
&[
"--changed-paths",
"crates/core/src/lib.rs",
"--changed-paths",
".changeset/feature.md",
]
)]
#[case::fails_when_changeset_targets_wrong_package(
"affected/single-package-wrong-target",
&[
"--changed-paths",
"crates/core/src/lib.rs",
"--changed-paths",
".changeset/feature.md",
]
)]
#[case::accepts_group_changeset_covering_member_package(
"affected/group-coverage",
&[
"--changed-paths",
"crates/core/src/lib.rs",
"--changed-paths",
".changeset/feature.md",
]
)]
#[case::accepts_group_changeset_covering_multiple_members(
"affected/group-coverage",
&[
"--changed-paths",
"crates/core/src/lib.rs",
"--changed-paths",
"crates/other/src/lib.rs",
"--changed-paths",
".changeset/feature.md",
]
)]
#[case::fails_when_changeset_targets_wrong_group(
"affected/group-coverage-missing",
&[
"--changed-paths",
"crates/core/src/lib.rs",
"--changed-paths",
".changeset/feature.md",
]
)]
#[case::reports_uncovered_packages_when_changeset_is_partial(
"affected/multi-package",
&[
"--changed-paths",
"crates/core/src/lib.rs",
"--changed-paths",
"crates/other/src/lib.rs",
"--changed-paths",
".changeset/feature.md",
]
)]
#[tokio::test(flavor = "multi_thread")]
async fn affected_scenarios_match_snapshot(#[case] fixture: &str, #[case] args: &[&str]) {
let mut settings = snapshot_settings();
settings.set_snapshot_suffix(current_test_name());
let _guard = settings.bind_to_scope();
let output = run_affected_json(&fixture_path(fixture), args).await;
assert_readable_json_snapshot!(output);
}
#[test]
fn affected_without_verify_flag_exits_zero_even_when_uncovered() {
let output = run_affected_raw(
&fixture_path("affected/single-package"),
&["--changed-paths", "crates/core/src/lib.rs"],
);
assert!(
output.status.success(),
"without --verify, exit code should be 0 even when packages are uncovered"
);
}
#[test]
fn affected_with_verify_flag_exits_nonzero_when_uncovered() {
let output = run_affected_raw(
&fixture_path("affected/single-package"),
&["--changed-paths", "crates/core/src/lib.rs", "--verify"],
);
assert!(
!output.status.success(),
"with --verify, exit code should be non-zero when packages are uncovered"
);
}
#[test]
fn affected_with_verify_flag_exits_zero_when_covered() {
let output = run_affected_raw(
&fixture_path("affected/single-package-with-changeset"),
&[
"--changed-paths",
"crates/core/src/lib.rs",
"--changed-paths",
".changeset/feature.md",
"--verify",
],
);
assert!(
output.status.success(),
"with --verify, the exit code should be 0 when all packages are covered: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
}
#[tokio::test(flavor = "multi_thread")]
async fn affected_since_flag_detects_changes_from_git_revision() {
let mut settings = snapshot_settings();
settings.set_snapshot_suffix(current_test_name());
let _guard = settings.bind_to_scope();
let tempdir = setup_scenario_workspace("affected/since-base");
let root = tempdir.path();
git(root, &["init"]);
git(root, &["config", "user.name", "Test"]);
git(root, &["config", "user.email", "test@test.com"]);
git(root, &["add", "."]);
git(root, &["commit", "-m", "initial"]);
copy_directory(&fixture_path("affected/since-changed-source"), root);
let json = run_affected_json(root, &["--from", "HEAD"]).await;
assert_readable_json_snapshot!(json);
}
#[tokio::test(flavor = "multi_thread")]
async fn affected_since_flag_detects_changeset_added_after_revision() {
let mut settings = snapshot_settings();
settings.set_snapshot_suffix(current_test_name());
let _guard = settings.bind_to_scope();
let tempdir = setup_scenario_workspace("affected/since-base");
let root = tempdir.path();
git(root, &["init"]);
git(root, &["config", "user.name", "Test"]);
git(root, &["config", "user.email", "test@test.com"]);
git(root, &["add", "."]);
git(root, &["commit", "-m", "initial"]);
copy_directory(&fixture_path("affected/since-changed-with-changeset"), root);
let json = run_affected_json(root, &["--from", "HEAD"]).await;
assert_readable_json_snapshot!(json);
}
#[tokio::test(flavor = "multi_thread")]
async fn affected_since_takes_priority_over_changed_paths_with_warning() {
let tempdir = setup_scenario_workspace("affected/since-base");
let root = tempdir.path();
git(root, &["init"]);
git(root, &["config", "user.name", "Test"]);
git(root, &["config", "user.email", "test@test.com"]);
git(root, &["add", "."]);
git(root, &["commit", "-m", "initial"]);
let output = run_affected_raw(
root,
&[
"--from",
"HEAD",
"--changed-paths",
"crates/core/src/lib.rs",
],
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--from takes priority") || stderr.contains("--changed-paths was ignored"),
"should warn when both flags are provided: stderr={stderr}"
);
}
async fn run_affected_json(root: &Path, args: &[&str]) -> Value {
let cli_args = std::iter::once(OsString::from("mc"))
.chain(std::iter::once(OsString::from("step:affected-packages")))
.chain(std::iter::once(OsString::from("--format")))
.chain(std::iter::once(OsString::from("json")))
.chain(args.iter().map(|value| OsString::from(*value)));
let output = monochange::run_with_args_in_dir("mc", cli_args, root)
.await
.unwrap_or_else(|error| panic!("command output: {error}"));
serde_json::from_str(&output)
.unwrap_or_else(|error| panic!("parse json: {error}\nstdout: {output}"))
}
fn run_affected_raw(root: &Path, args: &[&str]) -> std::process::Output {
monochange_command(None)
.current_dir(root)
.arg("step:affected-packages")
.arg("--format")
.arg("json")
.args(args)
.output()
.unwrap_or_else(|error| panic!("command output: {error}"))
}