use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use super::*;
use crate::contract::schema::{
Changelog, ChangelogMode, ChangelogSource, Contract, ContributionProvenance, DependencyBot,
Distribution, DistributionAdapter, DocsSite, Ecosystem, HealthBadge, Maturity, ProvenanceLevel,
Registry, Release, ReleaseLayout, ReleaseModel, Status, Target, VersioningBase,
};
use crate::ports::{CommandOutput, CommandRunner};
use crate::protocol::facts::{Facts, MaturitySignals};
#[derive(Default)]
struct FakeFs {
files: HashMap<PathBuf, Vec<u8>>,
dirs: HashSet<PathBuf>,
unreadable: HashSet<PathBuf>,
}
impl FakeFs {
fn file(mut self, path: &str, contents: &str) -> Self {
let p = PathBuf::from(path);
let mut cur = p.parent();
while let Some(dir) = cur {
if dir.as_os_str().is_empty() {
break;
}
self.dirs.insert(dir.to_path_buf());
cur = dir.parent();
}
self.files.insert(p, contents.as_bytes().to_vec());
self
}
fn dir(mut self, path: &str) -> Self {
self.dirs.insert(PathBuf::from(path));
self
}
fn unreadable_file(mut self, path: &str) -> Self {
let p = PathBuf::from(path);
if let Some(dir) = p.parent() {
self.dirs.insert(dir.to_path_buf());
}
self.files.insert(p.clone(), Vec::new());
self.unreadable.insert(p);
self
}
}
impl Fs for FakeFs {
fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
if self.unreadable.contains(path) {
return Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
}
self.files
.get(path)
.cloned()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))
}
fn exists(&self, path: &Path) -> bool {
self.files.contains_key(path) || self.dirs.contains(path)
}
fn is_dir(&self, path: &Path) -> bool {
self.dirs.contains(path)
}
fn is_file(&self, path: &Path) -> bool {
self.files.contains_key(path)
}
fn read_dir(&self, dir: &Path) -> std::io::Result<Vec<String>> {
if !self.dirs.contains(dir) {
return Err(std::io::Error::from(std::io::ErrorKind::NotFound));
}
let mut names: Vec<String> = self
.files
.keys()
.chain(self.dirs.iter())
.filter(|p| p.parent() == Some(dir))
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect();
names.sort();
Ok(names)
}
}
struct FakeCmd {
responses: HashMap<String, CommandOutput>,
calls: RefCell<Vec<String>>,
err_on_miss: bool,
}
impl FakeCmd {
fn new() -> Self {
Self {
responses: HashMap::new(),
calls: RefCell::new(Vec::new()),
err_on_miss: false,
}
}
fn key(program: &str, args: &[&str]) -> String {
format!("{program} {}", args.join(" "))
}
fn on(mut self, program: &str, args: &[&str], status: i32, stdout: &str, stderr: &str) -> Self {
self.responses.insert(
Self::key(program, args),
CommandOutput {
status: Some(status),
stdout: stdout.to_string(),
stderr: stderr.to_string(),
},
);
self
}
fn github(profile_json: &str) -> Self {
Self::new()
.on(
"git",
&["remote", "get-url", "origin"],
0,
"git@github.com:acme/tool.git\n",
"",
)
.on(
"gh",
&["api", "repos/acme/tool/community/profile"],
0,
profile_json,
"",
)
}
}
impl CommandRunner for FakeCmd {
fn run(&self, program: &str, args: &[&str], _cwd: &Path) -> std::io::Result<CommandOutput> {
let key = Self::key(program, args);
self.calls.borrow_mut().push(key.clone());
match self.responses.get(&key) {
Some(out) => Ok(out.clone()),
None if self.err_on_miss => Err(std::io::Error::from(std::io::ErrorKind::NotFound)),
None => Ok(CommandOutput {
status: Some(1),
stdout: String::new(),
stderr: "not found".to_string(),
}),
}
}
}
fn repo() -> &'static Path {
Path::new("/repo")
}
fn contract_at(maturity: Maturity) -> Contract {
Contract {
schema_version: 1,
status: Status::Approved,
maturity,
ecosystems: vec![Ecosystem::Rust],
targets: vec![Target {
ecosystem: Ecosystem::Rust,
package: Some("tool".to_string()),
registry: Registry::CratesIo,
adapter: crate::contract::schema::Adapter::CargoPublish,
}],
distribution: None,
versioning: VersioningBase::Semver,
versioning_pattern: None,
changelog: Changelog {
mode: ChangelogMode::Curated,
source: ChangelogSource::Manual,
fragment_dir: crate::contract::schema::DEFAULT_FRAGMENT_DIR.to_string(),
},
conventional_commits: false,
release: Release {
model: ReleaseModel::Gated,
layout: ReleaseLayout::Single,
},
contribution_provenance: ContributionProvenance::None,
provenance_level: ProvenanceLevel::None,
dependency_bot: DependencyBot::None,
health_badges: vec![],
license: "MIT".to_string(),
docs_site: DocsSite::None,
extra_fields: serde_json::Map::new(),
warnings: vec![],
}
}
fn dist_with(platforms: &[&str]) -> Distribution {
Distribution {
adapter: DistributionAdapter::CargoDist,
gh_releases: true,
installers: vec![],
homebrew_tap: None,
platforms: platforms.iter().map(|s| (*s).to_string()).collect(),
}
}
fn facts_with(maturity: Maturity, has_ci: bool, bot: Option<&str>) -> Facts {
Facts {
repo_root: "/repo".to_string(),
is_git: true,
has_commits: true,
ecosystems: vec![Ecosystem::Rust],
packages: vec![],
committers_total: 1,
committers_recent_year: 1,
tags: vec![],
has_semver_tag: false,
has_ge_1_0_release: false,
has_ci,
dependency_bot: bot.map(str::to_string),
has_issues_dir: false,
readme_self_label: None,
description: None,
maturity_signals: MaturitySignals {
production: false,
spike: false,
},
inferred_maturity: maturity,
}
}
fn ids(report: &AuditReport) -> Vec<&str> {
report.gaps.iter().map(|g| g.id.as_str()).collect()
}
fn gap<'a>(report: &'a AuditReport, id: &str) -> &'a Gap {
report
.gaps
.iter()
.find(|g| g.id == id)
.unwrap_or_else(|| panic!("expected a '{id}' gap, got {:?}", ids(report)))
}
const PROFILE_README_LICENSE: &str = r#"{"files":{
"readme":{"url":"x"},"license":{"key":"mit"},
"contributing":null,"code_of_conduct":null,
"issue_template":null,"pull_request_template":null,"security":null}}"#;
#[test]
fn empty_repo_at_mvp_fails_core_on_readme_license_ci() {
let fs = FakeFs::default();
let report = audit(
repo(),
&contract_at(Maturity::Mvp),
&facts_with(Maturity::Mvp, false, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert_eq!(report.core_complete, CoreStatus::Incomplete);
for id in ["readme", "license", "ci"] {
let g = gap(&report, id);
assert_eq!(g.category, Category::Core, "{id} is core");
assert_eq!(g.severity, Severity::Blocking, "{id} blocks at mvp");
}
}
#[test]
fn spike_gates_on_readme_license_only_ci_is_recommended() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract_at(Maturity::Spike),
&facts_with(Maturity::Spike, false, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert_eq!(report.core_complete, CoreStatus::Complete);
let ci = gap(&report, "ci");
assert_eq!(ci.category, Category::Canon);
assert_eq!(ci.severity, Severity::Recommended);
assert!(!ids(&report).contains(&"changelog"));
assert!(!ids(&report).contains(&"contributing"));
}
#[test]
fn complete_mvp_repo_has_no_core_gap() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file("/repo/CHANGELOG.md", "# Changelog\n")
.file("/repo/CONTRIBUTING.md", "# Contributing\n")
.file("/repo/CODE_OF_CONDUCT.md", "# CoC\n")
.file("/repo/SECURITY.md", "# Security\n")
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let report = audit(
repo(),
&contract_at(Maturity::Mvp),
&facts_with(Maturity::Mvp, true, Some("dependabot")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert_eq!(report.core_complete, CoreStatus::Complete);
assert!(
report.gaps.is_empty(),
"expected no gaps, got {:?}",
ids(&report)
);
}
#[test]
fn license_found_in_github_subdir() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/.github/LICENSE.md", "MIT\n");
let report = audit(
repo(),
&contract_at(Maturity::Spike),
&facts_with(Maturity::Spike, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert!(!ids(&report).contains(&"license"));
assert_eq!(report.core_complete, CoreStatus::Complete);
}
#[test]
fn mvp_reports_canon_gaps_when_absent() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let report = audit(
repo(),
&contract_at(Maturity::Mvp),
&facts_with(Maturity::Mvp, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert_eq!(report.core_complete, CoreStatus::Complete);
for id in [
"changelog",
"contributing",
"code-of-conduct",
"security-policy",
"dependency-bot",
] {
let g = gap(&report, id);
assert_eq!(g.category, Category::Canon, "{id} is canon");
assert_eq!(g.severity, Severity::Recommended, "{id} never blocks");
}
assert!(!ids(&report).contains(&"codeowners"));
assert!(!ids(&report).contains(&"architecture"));
}
#[test]
fn production_adds_hardening_canon_gaps() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file("/repo/CHANGELOG.md", "# Changelog\n")
.file("/repo/CONTRIBUTING.md", "# c\n")
.file("/repo/CODE_OF_CONDUCT.md", "# c\n")
.file("/repo/SECURITY.md", "# s\n")
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let report = audit(
repo(),
&contract_at(Maturity::Production),
&facts_with(Maturity::Production, true, Some("renovate")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert_eq!(report.core_complete, CoreStatus::Complete);
for id in [
"codeowners",
"governance",
"architecture",
"pre-commit",
"coverage",
] {
assert!(
ids(&report).contains(&id),
"expected {id} gap at production"
);
}
}
#[test]
fn fragment_changelog_without_dir_is_a_producer_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.changelog.mode = ChangelogMode::Fragment;
contract.changelog.source = ChangelogSource::IssuectlTrailers;
contract.changelog.fragment_dir = "changelog/fragments".to_string();
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, Some("dependabot")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "changelog-fragment-dir");
assert_eq!(g.category, Category::Producer);
assert_eq!(g.severity, Severity::Recommended);
assert!(g.detail.contains("changelog/fragments"));
}
#[test]
fn present_fragment_dir_yields_no_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.changelog.mode = ChangelogMode::Fragment;
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file("/repo/.github/workflows/ci.yml", "on: push\n")
.dir("/repo/changelog/fragments");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, Some("dependabot")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert!(!ids(&report).contains(&"changelog-fragment-dir"));
}
#[test]
fn coverage_badge_without_coverage_step_is_producer_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.health_badges = vec![HealthBadge::Coverage];
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file(
"/repo/.github/workflows/ci.yml",
"on: push\njobs:\n test:\n",
);
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, Some("dependabot")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "coverage");
assert_eq!(g.category, Category::Producer);
assert!(g.detail.contains("coverage"));
}
#[test]
fn coverage_badge_with_coverage_step_yields_no_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.health_badges = vec![HealthBadge::Coverage];
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file(
"/repo/.github/workflows/ci.yml",
"jobs:\n test:\n steps:\n - run: cargo llvm-cov\n",
);
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, Some("dependabot")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert!(!ids(&report).contains(&"coverage"));
}
#[test]
fn scorecard_badge_without_action_is_producer_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.health_badges = vec![HealthBadge::Scorecard];
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, Some("dependabot")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "scorecard");
assert_eq!(g.category, Category::Producer);
}
#[test]
fn registry_target_without_license_is_producer_gap() {
let mut contract = contract_at(Maturity::Spike);
contract.license = String::new(); let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n"); let report = audit(
repo(),
&contract,
&facts_with(Maturity::Spike, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "registry-license");
assert_eq!(g.category, Category::Producer);
assert_eq!(g.member, "oss-readme");
}
#[test]
fn distribution_without_linux_target_is_producer_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.distribution = Some(dist_with(&["aarch64-apple-darwin", "x86_64-apple-darwin"]));
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "distribution-linux");
assert_eq!(g.category, Category::Producer);
assert_eq!(g.severity, Severity::Recommended);
assert_eq!(g.member, "oss-init");
assert!(g.detail.contains("Linux"));
assert!(!ids(&report).contains(&"distribution-macos"));
}
#[test]
fn distribution_without_macos_target_is_producer_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.distribution = Some(dist_with(&["x86_64-unknown-linux-gnu"]));
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "distribution-macos");
assert_eq!(g.category, Category::Producer);
assert!(g.detail.contains("macOS"));
assert!(!ids(&report).contains(&"distribution-linux"));
}
#[test]
fn distribution_missing_both_oses_yields_two_gaps() {
let mut contract = contract_at(Maturity::Mvp);
contract.distribution = Some(dist_with(&["x86_64-pc-windows-msvc"]));
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert!(ids(&report).contains(&"distribution-linux"));
assert!(ids(&report).contains(&"distribution-macos"));
}
#[test]
fn android_triple_does_not_satisfy_the_linux_requirement() {
let mut contract = contract_at(Maturity::Mvp);
contract.distribution = Some(dist_with(&[
"aarch64-linux-android",
"aarch64-apple-darwin",
]));
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert!(ids(&report).contains(&"distribution-linux"));
assert!(!ids(&report).contains(&"distribution-macos"));
}
#[test]
fn distribution_with_linux_target_yields_no_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.distribution = Some(dist_with(&[
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"aarch64-unknown-linux-musl",
"x86_64-unknown-linux-musl",
]));
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert!(!ids(&report).contains(&"distribution-linux"));
assert!(!ids(&report).contains(&"distribution-macos"));
}
#[test]
fn platform_triple_classifiers() {
for t in ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-musl"] {
assert!(is_linux_triple(t), "{t} is Linux");
assert!(!is_darwin_triple(t), "{t} is not macOS");
}
for t in ["aarch64-apple-darwin", "x86_64-apple-darwin"] {
assert!(is_darwin_triple(t), "{t} is macOS");
assert!(!is_linux_triple(t), "{t} is not Linux");
}
for t in [
"x86_64-pc-windows-msvc",
"aarch64-linux-android", "aarch64-apple-ios", "wasm32-unknown-unknown",
] {
assert!(!is_linux_triple(t), "{t} is not desktop-Linux");
assert!(!is_darwin_triple(t), "{t} is not macOS");
}
}
#[test]
fn distribution_with_gnu_linux_target_yields_no_gap() {
let mut contract = contract_at(Maturity::Mvp);
contract.distribution = Some(dist_with(&[
"aarch64-apple-darwin",
"x86_64-unknown-linux-gnu",
]));
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert!(!ids(&report).contains(&"distribution-linux"));
assert!(!ids(&report).contains(&"distribution-macos"));
}
#[test]
fn no_distribution_block_yields_no_cross_platform_gap() {
let contract = contract_at(Maturity::Production); let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Production, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert!(!ids(&report).contains(&"distribution-linux"));
assert!(!ids(&report).contains(&"distribution-macos"));
}
#[test]
fn distribution_without_linux_escalates_wording_at_production() {
let mut contract = contract_at(Maturity::Production);
contract.distribution = Some(dist_with(&["aarch64-apple-darwin"]));
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Production, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "distribution-linux");
assert!(g.detail.contains("required"), "detail: {}", g.detail);
}
#[test]
fn community_profile_parsed_on_success() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract_at(Maturity::Spike),
&facts_with(Maturity::Spike, true, None),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let cp = &report.community_profile;
assert!(cp.checked);
assert_eq!(cp.unavailable_reason, None);
assert_eq!(cp.readme, Presence::Present);
assert_eq!(cp.license, Presence::Present);
assert_eq!(cp.contributing, Presence::Absent);
assert_eq!(cp.security, Presence::Absent);
}
#[test]
fn gh_api_failure_yields_unknown_never_false() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let cmd = FakeCmd::new()
.on(
"git",
&["remote", "get-url", "origin"],
0,
"git@github.com:acme/tool.git\n",
"",
)
.on(
"gh",
&["api", "repos/acme/tool/community/profile"],
1,
"",
"gh: Not Found (HTTP 404)\n",
);
let report = audit(
repo(),
&contract_at(Maturity::Spike),
&facts_with(Maturity::Spike, true, None),
&fs,
&cmd,
);
let cp = &report.community_profile;
assert!(!cp.checked, "a failed lookup is not 'checked'");
for p in [
cp.readme,
cp.license,
cp.contributing,
cp.code_of_conduct,
cp.issue_template,
cp.pull_request_template,
cp.security,
] {
assert_eq!(
p,
Presence::Unknown,
"outage must yield unknown, not absent"
);
}
assert!(cp.unavailable_reason.as_deref().unwrap().contains("404"));
}
#[test]
fn non_github_remote_yields_unchecked_profile() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let cmd = FakeCmd::new().on(
"git",
&["remote", "get-url", "origin"],
0,
"git@gitlab.com:acme/tool.git\n",
"",
);
let report = audit(
repo(),
&contract_at(Maturity::Spike),
&facts_with(Maturity::Spike, true, None),
&fs,
&cmd,
);
assert!(!report.community_profile.checked);
assert_eq!(report.community_profile.readme, Presence::Unknown);
assert!(!cmd.calls.borrow().iter().any(|c| c.starts_with("gh api")));
}
#[test]
fn no_remote_yields_unchecked_profile() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let cmd = FakeCmd::new().on("git", &["remote", "get-url", "origin"], 1, "", "error");
let report = audit(
repo(),
&contract_at(Maturity::Spike),
&facts_with(Maturity::Spike, true, None),
&fs,
&cmd,
);
assert!(!report.community_profile.checked);
}
#[test]
fn coverage_probe_read_failure_yields_unknown_not_absent() {
let mut contract = contract_at(Maturity::Mvp);
contract.health_badges = vec![HealthBadge::Coverage];
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.unreadable_file("/repo/.github/workflows/ci.yml");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, Some("dependabot")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "coverage");
assert_eq!(
g.status,
Presence::Unknown,
"an unreadable workflow must yield unknown, not absent"
);
}
#[test]
fn coverage_probe_ignores_non_yaml_files() {
let mut contract = contract_at(Maturity::Mvp);
contract.health_badges = vec![HealthBadge::Coverage];
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file("/repo/.github/workflows/notes.txt", "coverage is planned\n");
let report = audit(
repo(),
&contract,
&facts_with(Maturity::Mvp, true, Some("dependabot")),
&fs,
&FakeCmd::github(PROFILE_README_LICENSE),
);
let g = gap(&report, "coverage");
assert_eq!(g.status, Presence::Absent, "a .txt file is not a workflow");
}
#[test]
fn community_profile_missing_files_object_is_unknown_not_absent() {
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let cmd = FakeCmd::new()
.on(
"git",
&["remote", "get-url", "origin"],
0,
"git@github.com:acme/tool.git\n",
"",
)
.on(
"gh",
&["api", "repos/acme/tool/community/profile"],
0,
r#"{"message":"rate limited"}"#,
"",
);
let report = audit(
repo(),
&contract_at(Maturity::Spike),
&facts_with(Maturity::Spike, true, None),
&fs,
&cmd,
);
let cp = &report.community_profile;
assert!(!cp.checked, "no files object ⇒ not a successful check");
assert_eq!(cp.readme, Presence::Unknown);
assert_eq!(cp.security, Presence::Unknown);
}
#[test]
fn community_profile_reads_security_policy_alias() {
let profile = r#"{"files":{
"readme":{"url":"x"},"license":{"key":"mit"},
"security_policy":{"url":"y"}}}"#;
let fs = FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n");
let report = audit(
repo(),
&contract_at(Maturity::Spike),
&facts_with(Maturity::Spike, true, None),
&fs,
&FakeCmd::github(profile),
);
assert_eq!(report.community_profile.security, Presence::Present);
}
#[test]
fn enum_wire_strings_are_stable() {
let cases = [
(
serde_json::to_string(&Presence::Present).unwrap(),
"\"present\"",
),
(
serde_json::to_string(&Presence::Absent).unwrap(),
"\"absent\"",
),
(
serde_json::to_string(&Presence::Unknown).unwrap(),
"\"unknown\"",
),
(
serde_json::to_string(&CoreStatus::Complete).unwrap(),
"\"complete\"",
),
(
serde_json::to_string(&CoreStatus::Incomplete).unwrap(),
"\"incomplete\"",
),
(serde_json::to_string(&Category::Core).unwrap(), "\"core\""),
(
serde_json::to_string(&Category::Canon).unwrap(),
"\"canon\"",
),
(
serde_json::to_string(&Category::Producer).unwrap(),
"\"producer\"",
),
(
serde_json::to_string(&Severity::Blocking).unwrap(),
"\"blocking\"",
),
(
serde_json::to_string(&Severity::Recommended).unwrap(),
"\"recommended\"",
),
];
for (got, want) in cases {
assert_eq!(got, want, "wire string drift");
}
assert_eq!(Presence::Unknown.as_str(), "unknown");
assert_eq!(CoreStatus::Incomplete.as_str(), "incomplete");
assert_eq!(Category::Producer.as_str(), "producer");
assert_eq!(Severity::Blocking.as_str(), "blocking");
}
#[test]
fn same_inputs_same_report() {
let build_fs = || {
FakeFs::default()
.file("/repo/README.md", "# tool\n")
.file("/repo/LICENSE", "MIT\n")
.file("/repo/.github/workflows/ci.yml", "on: push\n")
};
let a = audit(
repo(),
&contract_at(Maturity::Mvp),
&facts_with(Maturity::Mvp, true, None),
&build_fs(),
&FakeCmd::github(PROFILE_README_LICENSE),
);
let b = audit(
repo(),
&contract_at(Maturity::Mvp),
&facts_with(Maturity::Mvp, true, None),
&build_fs(),
&FakeCmd::github(PROFILE_README_LICENSE),
);
assert_eq!(
serde_json::to_string(&a).unwrap(),
serde_json::to_string(&b).unwrap()
);
}