use std::path::{Path, PathBuf};
use std::process::Command;
const CONFIG: &str = r#"
[stages]
environments = ["main", "release/*"]
[[tests.runners]]
id = "unit"
match = ["test/**"]
command = ["sh", "-c", "exit 0", "sh", "{files}"]
from = "draft"
[[tests.runners]]
id = "e2e"
match = ["e2e/**"]
command = ["sh", "-c", "exit 0", "sh", "{files}"]
from = "merge"
[[checks]]
id = "desktop-build"
command = ["sh", "-c", "exit 0"]
paths = ["src/**"]
from = "merge"
"#;
const WORKFLOW: &str = ".github/workflows/fairlead.yml";
fn fixture() -> String {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ci-workflow/fairlead.yml");
std::fs::read_to_string(path).unwrap().replace("\r\n", "\n")
}
fn repo(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("fairlead-workflow-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let git = Command::new("git")
.args(["init", "-q", "-b", "main"])
.current_dir(&dir)
.env("GIT_CONFIG_NOSYSTEM", "1")
.output()
.unwrap();
assert!(git.status.success());
std::fs::write(dir.join("fairlead.toml"), CONFIG).unwrap();
dir
}
fn workflow(dir: &Path, args: &[&str]) -> (i32, String, String) {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_fairlead"));
cmd.args(["ci", "workflow"])
.args(args)
.current_dir(dir)
.env_clear();
if let Some(path) = std::env::var_os("PATH") {
cmd.env("PATH", path);
}
let out = cmd.env("HOME", dir).output().unwrap();
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn unversioned(text: &str) -> String {
let pinned = format!("version: v{}\n", env!("CARGO_PKG_VERSION"));
text.replace(&pinned, "version: vX.Y.Z\n")
}
#[test]
fn the_workflow_for_a_staged_config_is_the_fixture_and_names_each_gated_jobs_if_line() {
let dir = repo("print");
let (code, out, err) = workflow(&dir, &[]);
assert_eq!(code, 0, "{err}");
assert_eq!(unversioned(&out), fixture());
assert!(
err.contains(" if: needs.plan.outputs.run_e2e == 'true' # e2e\n"),
"{err}"
);
assert!(
err.contains(" if: needs.plan.outputs.run_desktop_build == 'true' # desktop-build\n"),
"{err}"
);
assert!(!err.contains("run_unit"), "{err}");
assert!(!dir.join(WORKFLOW).exists());
}
#[test]
fn write_refuses_a_hand_written_file_and_rewrites_its_own() {
let dir = repo("write");
let path = dir.join(WORKFLOW);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "name: ours\n").unwrap();
let (code, _, err) = workflow(&dir, &["--write"]);
assert_eq!(code, 2, "{err}");
assert!(
err.contains(".github/workflows/fairlead.yml wasn't written by `fairlead ci workflow`"),
"{err}"
);
assert_eq!(std::fs::read_to_string(&path).unwrap(), "name: ours\n");
let stale = "# Generated by `fairlead ci workflow` from the Fairlead config.\nname: old\n";
std::fs::write(&path, stale).unwrap();
let (code, out, err) = workflow(&dir, &["--write"]);
assert_eq!(code, 0, "{err}");
assert!(
out.starts_with("fairlead: rewrote .github/workflows/fairlead.yml\n"),
"{out}"
);
assert!(out.contains("run_e2e == 'true'"), "{out}");
assert_eq!(
unversioned(&std::fs::read_to_string(&path).unwrap()),
fixture()
);
let (code, out, _) = workflow(&dir, &["--write"]);
assert_eq!(code, 0);
assert!(out.starts_with("fairlead: unchanged"), "{out}");
}
#[test]
fn write_creates_the_workflow_where_there_is_none() {
let dir = repo("fresh");
let (code, out, err) = workflow(&dir, &["--write"]);
assert_eq!(code, 0, "{err}");
assert!(
out.starts_with("fairlead: wrote .github/workflows/fairlead.yml\n"),
"{out}"
);
let text = std::fs::read_to_string(dir.join(WORKFLOW)).unwrap();
assert_eq!(unversioned(&text), fixture());
}
#[test]
fn the_workflow_pins_actions_as_this_repositorys_own_workflows_do() {
let workflows = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.github/workflows");
let ours: String = std::fs::read_dir(&workflows)
.unwrap()
.flatten()
.map(|e| std::fs::read_to_string(e.path()).unwrap_or_default())
.collect();
let text = fixture();
let pins: Vec<&str> = text
.lines()
.filter_map(|l| l.trim().strip_prefix("- uses: "))
.filter_map(|u| u.split_whitespace().next())
.collect();
assert!(pins.len() >= 3, "{pins:?}");
for pin in pins {
let (_, sha) = pin.split_once('@').unwrap();
assert_eq!(sha.len(), 40, "{pin} isn't pinned to a commit");
assert!(
ours.contains(pin),
"{pin} differs from this repository's own pin"
);
}
}
#[test]
fn the_insights_workflow_runs_weekly_runs_no_tests_and_is_written_beside_the_test_workflow() {
let dir = repo("insights");
let (code, text, err) = workflow(&dir, &["--insights"]);
assert_eq!(code, 0, "{err}");
assert!(
text.starts_with("# Generated by `fairlead ci workflow` --insights"),
"{text}"
);
assert!(
text.contains(" schedule:\n - cron: \"17 5 * * 1\""),
"{text}"
);
for scope in ["contents", "actions", "pull-requests", "checks"] {
assert!(
text.contains(&format!(" {scope}: read")),
"replay fetch needs {scope}: {text}"
);
}
assert!(
!text.contains("fairlead ci run") && !text.contains("pull_request"),
"a schedule here starts no test run: {text}"
);
assert!(text
.contains("fairlead replay fetch --repo \"$GITHUB_REPOSITORY\" --workflow fairlead.yml"));
assert!(text.contains("fairlead insights --since"));
let line = text
.lines()
.map(str::trim)
.find(|l| l.starts_with("fairlead replay run "))
.expect("the job replays");
assert!(line.contains("--config fairlead.toml"), "{line}");
let args: Vec<String> = line
.split_whitespace()
.skip(1)
.take_while(|a| *a != ">")
.map(|a| {
a.trim_matches('"')
.replace("$RUNNER_TEMP", &dir.display().to_string())
})
.collect();
let out = std::process::Command::new(env!("CARGO_BIN_EXE_fairlead"))
.args(&args)
.current_dir(&dir)
.output()
.unwrap();
let err = String::from_utf8_lossy(&out.stderr);
assert!(
!err.contains("required arguments were not provided"),
"{err}"
);
assert!(!err.contains("unexpected argument"), "{err}");
let (code, out, _) = workflow(&dir, &["--insights", "--write"]);
assert_eq!(code, 0);
assert!(
out.contains("wrote .github/workflows/fairlead-insights.yml"),
"{out}"
);
assert!(
!dir.join(".github/workflows/fairlead.yml").exists(),
"the test workflow is a separate file"
);
let (_, out, _) = workflow(&dir, &["--insights", "--write"]);
assert!(out.contains("unchanged"), "{out}");
std::fs::write(
dir.join(".github/workflows/fairlead-insights.yml"),
"name: ours\n",
)
.unwrap();
let (code, _, err) = workflow(&dir, &["--insights", "--write"]);
assert_eq!(code, 2);
assert!(
err.contains("wasn't written by `fairlead ci workflow`"),
"{err}"
);
}