use super::util::*;
use crate::config::PackageConfig;
mod cache_flow {
use crate::cache;
use crate::git::open_repo;
use crate::test_utils::{commit_file, init_repo, with_cwd};
use crate::timing::Timing;
fn setup(dir: &std::path::Path) {
std::fs::write(
dir.join("Cargo.toml"),
"[package]\nname = \"app\"\nversion = \"1.0.0\"\n",
)
.unwrap();
std::fs::write(
dir.join(".ferrflow"),
r#"{"package":[{"name":"app","path":".","versionedFiles":[{"path":"Cargo.toml","format":"toml"}]}]}"#,
)
.unwrap();
}
fn run_check(dir: &std::path::Path) {
let config = dir.join(".ferrflow");
with_cwd(dir, || {
super::super::check(
Some(&config),
false,
false,
None,
false,
&mut Timing::new(false),
)
})
.unwrap();
}
#[test]
fn first_check_writes_cache_second_hits_with_same_key() {
let (dir, _repo) = init_repo();
setup(dir.path());
commit_file(dir.path(), "a.txt", "x", "feat: a", 1_950_000_000);
run_check(dir.path());
let repo = open_repo(dir.path()).unwrap();
let cache_dir = cache::cache_dir(&repo);
let key = cache::compute_key(
&repo,
dir.path(),
Some(&dir.path().join(".ferrflow")),
"text",
)
.expect("key");
let first = cache::read(&cache_dir, &key).expect("cache written on miss");
run_check(dir.path());
let second = cache::read(&cache_dir, &key).expect("still a hit");
assert_eq!(first.text_lines, second.text_lines);
assert!(!first.text_lines.is_empty());
}
#[test]
fn cache_busts_after_a_new_commit() {
let (dir, _repo) = init_repo();
setup(dir.path());
commit_file(dir.path(), "a.txt", "x", "feat: a", 1_950_000_100);
run_check(dir.path());
let repo = open_repo(dir.path()).unwrap();
let cache_dir = cache::cache_dir(&repo);
let old_key = cache::compute_key(
&repo,
dir.path(),
Some(&dir.path().join(".ferrflow")),
"text",
)
.unwrap();
commit_file(dir.path(), "b.txt", "y", "feat: b", 1_950_000_200);
let repo2 = open_repo(dir.path()).unwrap();
let new_key = cache::compute_key(
&repo2,
dir.path(),
Some(&dir.path().join(".ferrflow")),
"text",
)
.unwrap();
assert!(cache::read(&cache_dir, &new_key).is_none());
assert!(cache::read(&cache_dir, &old_key).is_some());
}
}
#[test]
fn pick_higher_semver_prefers_tag_when_tag_is_higher() {
assert_eq!(pick_higher_semver("2.0.0", "3.0.0"), "3.0.0");
}
#[test]
fn pick_higher_semver_prefers_file_when_file_is_higher() {
assert_eq!(pick_higher_semver("5.0.0", "2.0.0"), "5.0.0");
}
#[test]
fn pick_higher_semver_returns_tag_on_equality() {
assert_eq!(pick_higher_semver("2.1.0", "2.1.0"), "2.1.0");
}
#[test]
fn pick_higher_semver_falls_back_to_tag_when_file_is_invalid() {
assert_eq!(pick_higher_semver("garbage", "2.0.0"), "2.0.0");
}
#[test]
fn pick_higher_semver_falls_back_to_file_when_tag_is_invalid() {
assert_eq!(pick_higher_semver("2.0.0", "garbage"), "2.0.0");
}
#[test]
fn pick_higher_semver_strips_leading_v() {
assert_eq!(pick_higher_semver("v2.0.0", "v3.0.0"), "v3.0.0");
}
fn make_pkg(name: &str, path: &str, shared: &[&str]) -> PackageConfig {
PackageConfig {
name: name.into(),
path: path.into(),
versioned_files: vec![],
changelog: None,
shared_paths: shared.iter().map(|s| s.to_string()).collect(),
depends_on: vec![],
versioning: None,
tag_template: None,
hooks: None,
floating_tags: None,
publishers: vec![],
}
}
#[test]
fn single_package_always_touched() {
let pkg = make_pkg("app", ".", &[]);
let files = vec!["README.md".to_string()];
assert!(is_package_touched(&pkg, &files, false));
}
#[test]
fn monorepo_root_package_always_touched() {
let pkg = make_pkg("root", ".", &[]);
let files = vec!["something.rs".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_package_touched_by_own_files() {
let pkg = make_pkg("api", "packages/api", &[]);
let files = vec!["packages/api/src/main.rs".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_package_not_touched_by_other_files() {
let pkg = make_pkg("api", "packages/api", &[]);
let files = vec!["packages/site/index.ts".to_string()];
assert!(!is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_package_touched_by_shared_path() {
let pkg = make_pkg("api", "packages/api", &["packages/shared/"]);
let files = vec!["packages/shared/types.ts".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_shared_path_trailing_slash_trimmed() {
let pkg = make_pkg("api", "packages/api", &["lib/"]);
let files = vec!["lib/utils.rs".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_no_changed_files() {
let pkg = make_pkg("api", "packages/api", &[]);
let files: Vec<String> = vec![];
assert!(!is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_path_with_dot_slash_prefix() {
let pkg = make_pkg("api", "./packages/api", &[]);
let files = vec!["packages/api/src/main.rs".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn single_package_mode_always_touched() {
let pkg = make_pkg("api", "packages/api", &[]);
let files = vec!["unrelated/file.rs".to_string()];
assert!(is_package_touched(&pkg, &files, false));
}
#[test]
fn monorepo_empty_path_is_root() {
let pkg = make_pkg("root", "", &[]);
let files = vec!["anything.rs".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_exact_shared_path_file() {
let pkg = make_pkg("api", "packages/api", &["shared-config.json"]);
let files = vec!["shared-config.json".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_multiple_shared_paths() {
let pkg = make_pkg("api", "packages/api", &["lib/", "proto/"]);
let files = vec!["proto/schema.proto".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_similar_prefix_no_false_positive() {
let pkg = make_pkg("api", "packages/api", &[]);
let files = vec!["packages/api-docs/README.md".to_string()];
assert!(!is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_shared_path_with_trailing_slash() {
let pkg = make_pkg("api", "packages/api", &["packages/shared/"]);
let files = vec!["packages/shared/types.ts".to_string()];
assert!(is_package_touched(&pkg, &files, true));
}
#[test]
fn monorepo_empty_changed_files_single_package() {
let pkg = make_pkg("app", "packages/app", &[]);
let files: Vec<String> = vec![];
assert!(is_package_touched(&pkg, &files, false));
}
#[test]
fn parse_force_version_single_repo() {
let fv = "1.2.3";
let result: Option<(Option<&str>, &str)> = if let Some(at_pos) = fv.find('@') {
let name = &fv[..at_pos];
let version = &fv[at_pos + 1..];
Some((Some(name), version))
} else {
Some((None, fv))
};
assert_eq!(result, Some((None, "1.2.3")));
}
#[test]
fn parse_force_version_monorepo() {
let fv = "api@2.0.0";
let result: Option<(Option<&str>, &str)> = if let Some(at_pos) = fv.find('@') {
let name = &fv[..at_pos];
let version = &fv[at_pos + 1..];
Some((Some(name), version))
} else {
Some((None, fv))
};
assert_eq!(result, Some((Some("api"), "2.0.0")));
}
#[test]
fn parse_force_version_with_v_prefix() {
let fv = "v3.0.0";
let clean = fv.strip_prefix('v').unwrap_or(fv);
assert!(semver::Version::parse(clean).is_ok());
}
#[test]
fn parse_force_version_invalid_semver() {
let fv = "not-a-version";
let clean = fv.strip_prefix('v').unwrap_or(fv);
assert!(semver::Version::parse(clean).is_err());
}