use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use repon_core::{ActionSpec, Core, CoreSpec, RepoOverride, SetSpec, Step};
fn git(path: &Path, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
.arg(path)
.args(["-c", "user.email=test@example.com", "-c", "user.name=Test"])
.args(args)
.status()
.expect("run git");
assert!(status.success(), "git {args:?} failed");
}
fn init_repo_with_a_commit(path: &Path) {
std::fs::create_dir_all(path).expect("create repo dir");
git(path, &["init", "--quiet"]);
git(path, &["commit", "--allow-empty", "-m", "first"]);
}
fn spec(roots: Vec<PathBuf>) -> CoreSpec {
CoreSpec {
set: SetSpec {
name: "test".to_string(),
roots,
include: Vec::new(),
exclude: Vec::new(),
},
overrides: Vec::<RepoOverride>::new(),
poll_interval: Duration::from_secs(3600),
status_stale_after: Duration::from_secs(3600),
generation_deadline: Duration::from_secs(3600),
show_submodules: false,
fetch: repon_core::FetchSpec {
enabled: false,
interval: Duration::from_secs(3600),
concurrency: 4,
},
auto_update: repon_core::AutoUpdateSpec { enabled: false },
}
}
fn poll_for_signal_command(signal: &Path, attempts: u32) -> String {
format!(
"i=0; while [ ! -f \"{}\" ] && [ \"$i\" -lt {attempts} ]; do sleep 0.1; i=$((i+1)); done",
signal.display()
)
}
#[test]
fn the_actions_own_pool_never_starves_a_refresh_dispatched_on_the_global_pool_while_it_blocks() {
rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build_global()
.expect(
"this integration test binary's very first use of rayon's global pool; if this \
fails, something before it now touches the pool first",
);
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().canonicalize().expect("canonicalize temp dir");
let blocked_a = root.join("blocked-a");
let blocked_b = root.join("blocked-b");
let probed = root.join("probed");
init_repo_with_a_commit(&blocked_a);
init_repo_with_a_commit(&blocked_b);
init_repo_with_a_commit(&probed);
let core = Core::start(spec(vec![root.clone()]));
let snapshot = core.settle();
let key_of = |path: &Path| {
snapshot
.entities
.iter()
.find(|entity| entity.key.path() == path)
.unwrap_or_else(|| panic!("entity at {path:?} discovered"))
.key
.clone()
};
let blocked_keys = vec![key_of(&blocked_a), key_of(&blocked_b)];
let probed_key = key_of(&probed);
let signal = root.join("signal");
let block_command = poll_for_signal_command(&signal, 600);
let action = ActionSpec {
label: Arc::from("block-both-workers"),
name: Some(Arc::from("block-both-workers")),
steps: vec![Step {
argv: vec!["sh".to_string(), "-c".to_string(), block_command],
shell: false,
interactive: false,
env: Vec::new(),
}],
concurrency: 2,
when: None,
};
let started = core.run_action(action, &blocked_keys);
assert!(started, "the Action fan-out must start");
core.refresh(std::slice::from_ref(&probed_key));
let settled = core.settle();
let probe_settled = settled
.entities
.iter()
.find(|entity| entity.key == probed_key)
.is_some_and(|entity| entity.branch.settled().is_some());
std::fs::write(&signal, b"go").expect("write the signal file");
assert!(
probe_settled,
"a probe on the global pool must settle while the Action's own two steps are still \
blocked waiting for a signal this test has not written yet; a fan-out wrongly sharing \
the global pool leaves no worker free for it to run on at all"
);
}