use std::io::{Read, Write as _};
use std::net::TcpListener;
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, LazyLock, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use onetaskgraph_live::artifact::{Registration, Registry, Run, Sweep};
use onetaskgraph_live::{Credential, Exclusivity, Session};
use serde_json::{Value, json};
#[allow(dead_code)]
mod journey;
#[allow(dead_code)]
mod lane;
use lane::{SESSION_NAME, artifact_label, artifact_title};
const BOARD: &str = "PVT_board";
const REPOSITORY: &str = "acme/work";
const TOKEN: &str = "test-token";
const WINDOW: Duration = Duration::from_secs(60);
const PATIENCE: Duration = Duration::from_secs(30);
const CHILD_VARIABLE: &str = "ONETASKGRAPH_SWEEP_GATE_CHILD";
const NOW: u64 = 1_787_816_134_627_361;
fn aged(windows: u64) -> u64 {
NOW - windows * u64::try_from(WINDOW.as_micros()).expect("a minute fits in microseconds")
}
struct Runs {
directory: PathBuf,
registry: Registry,
mine: Run,
}
static RUNS: LazyLock<Runs> = LazyLock::new(|| {
let directory =
std::env::temp_dir().join(format!("onetaskgraph-sweep-gate-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&directory);
let registry = Registry::at(&directory);
let mine = registry.enrol();
assert!(
mine.host().is_some(),
"a registry this check can write is what every drive below decides against"
);
Runs {
directory,
registry,
mine,
}
});
fn ended_run(offset: u32) -> Run {
let registration = Registration::take(&RUNS.registry, process(40_000 + offset))
.expect("a run that this check then ends");
let run = registration.run();
drop(registration);
run
}
fn process(number: u32) -> NonZeroU32 {
NonZeroU32::new(number).expect("a process id this check names is never zero")
}
fn sweep_at(now: u64) -> Sweep {
RUNS.registry.sweep(RUNS.mine, now, WINDOW)
}
struct LiveRunBeside {
child: Child,
run: Run,
}
impl LiveRunBeside {
fn start() -> Self {
let child = Command::new(
std::env::current_exe().expect("the path of the test binary being re-executed"),
)
.args([
"--exact",
"a_run_of_this_binary_is_registered_and_the_registry_says_it_is_live",
])
.env(CHILD_VARIABLE, &RUNS.directory)
.env(
onetaskgraph_live::artifact::REGISTRY_DIRECTORY_VARIABLE,
&RUNS.directory,
)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("a second process for the live run beside this sweep");
let run = Run::vouched(
RUNS.mine
.host()
.expect("the registry every drive here decides against"),
process(child.id()),
);
let started = Instant::now();
while !ready_marker(process(child.id())).exists() {
assert!(
started.elapsed() < PATIENCE,
"the live run beside this sweep never registered"
);
thread::sleep(Duration::from_millis(20));
}
Self { child, run }
}
fn end(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let started = Instant::now();
while !RUNS
.registry
.finished_runs()
.contains(&process(self.child.id()))
{
assert!(
started.elapsed() < PATIENCE,
"the kernel never released the ended run's registration"
);
thread::sleep(Duration::from_millis(20));
}
}
}
impl Drop for LiveRunBeside {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
fn a_run_of_this_binary_is_registered_and_the_registry_says_it_is_live() {
let run = Run::current();
assert!(
run.host().is_some(),
"the registry this run was pointed at could not vouch for it"
);
assert!(
!Registry::shared().finished_runs().contains(&run.process()),
"the registry reported a run that is running as one that has ended"
);
let Some(directory) = std::env::var_os(CHILD_VARIABLE) else {
return;
};
std::fs::write(
PathBuf::from(directory).join(format!("ready-{}", run.process())),
b"",
)
.expect("saying that this run has registered");
thread::sleep(PATIENCE * 4);
}
fn ready_marker(process: NonZeroU32) -> PathBuf {
RUNS.directory.join(format!("ready-{process}"))
}
#[derive(Default)]
struct Board {
items: Vec<(String, Option<String>, String)>,
issues: Vec<String>,
labels: Vec<String>,
vanishing_items: Vec<String>,
vanishing_labels: Vec<String>,
immortal: Vec<String>,
refused: Vec<String>,
}
impl Board {
fn let_the_item_race_happen(&mut self) {
let taken = std::mem::take(&mut self.vanishing_items);
self.items.retain(|(id, _, _)| !taken.contains(id));
self.issues
.retain(|issue| !taken.iter().any(|item| issue_of(item) == *issue));
self.vanishing_items = taken;
}
fn let_the_label_race_happen(&mut self) {
let taken = std::mem::take(&mut self.vanishing_labels);
self.labels.retain(|name| !taken.contains(name));
self.vanishing_labels = taken;
}
fn immortal(&self, what: &str) -> bool {
self.immortal.iter().any(|held| held == what)
}
fn holds_item(&self, id: &str) -> bool {
self.items.iter().any(|(item, _, _)| item == id)
}
fn holds_issue(&self, id: &str) -> bool {
self.issues.iter().any(|issue| issue == id)
}
fn holds_label(&self, name: &str) -> bool {
self.labels.iter().any(|held| held == name)
}
fn titles(&self) -> Vec<String> {
sorted(
self.items
.iter()
.map(|(_, _, title)| title.clone())
.collect(),
)
}
fn label_names(&self) -> Vec<String> {
sorted(self.labels.clone())
}
}
fn issue_of(item: &str) -> String {
item.replace("PVTI_", "I_")
}
static STANDIN: LazyLock<Arc<Mutex<Board>>> = LazyLock::new(|| {
let board = Arc::new(Mutex::new(Board::default()));
let host = serve(Arc::clone(&board));
journey::against(journey::Endpoints {
graphql: format!("{host}/graphql"),
rest_host: host,
source: None,
});
board
});
static ONE_AT_A_TIME: Mutex<()> = Mutex::new(());
struct Drive {
_exclusive: std::sync::MutexGuard<'static, ()>,
}
impl Drive {
fn plant(
items: Vec<(&str, Option<&str>, String)>,
labels: Vec<String>,
vanishing_items: Vec<String>,
vanishing_labels: Vec<String>,
immortal: Vec<String>,
) -> Self {
let exclusive = ONE_AT_A_TIME
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let items: Vec<_> = items
.into_iter()
.map(|(item, issue, title)| (item.to_owned(), issue.map(str::to_owned), title))
.collect();
*STANDIN
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Board {
issues: items
.iter()
.filter_map(|(_, issue, _)| issue.clone())
.collect(),
items,
labels,
vanishing_items,
vanishing_labels,
immortal,
refused: Vec::new(),
};
Self {
_exclusive: exclusive,
}
}
fn left(&self) -> (Vec<String>, Vec<String>) {
let board = STANDIN
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
(board.titles(), board.label_names())
}
fn refused(&self) -> Vec<String> {
STANDIN
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.refused
.clone()
}
fn holds_issue(&self, id: &str) -> bool {
STANDIN
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.holds_issue(id)
}
}
fn session_in_flight() -> Session {
Session::open(
SESSION_NAME,
Credential::new(TOKEN).expect("a placeholder credential is not blank"),
Exclusivity::Shared,
)
.unwrap_or_else(|declined| {
panic!(
"this lane takes no seat, so nothing may decline it here: {}",
declined.message()
)
})
}
#[tokio::test]
async fn a_sweep_leaves_a_concurrent_live_runs_artifacts_however_old_they_are() {
let mut beside = LiveRunBeside::start();
let theirs = artifact_title(beside.run, aged(10));
let their_label = artifact_label(beside.run, aged(10));
let mine = artifact_title(RUNS.mine, aged(10));
let my_label = artifact_label(RUNS.mine, aged(10));
let ended = ended_run(1);
let stale = artifact_title(ended, aged(2));
let stale_label = artifact_label(ended, aged(2));
let fresh_ended = ended_run(2);
let fresh = artifact_title(fresh_ended, NOW);
let fresh_label = artifact_label(fresh_ended, NOW);
let drive = Drive::plant(
vec![
("PVTI_theirs", Some("I_theirs"), theirs.clone()),
("PVTI_mine", Some("I_mine"), mine.clone()),
("PVTI_stale", Some("I_stale"), stale.clone()),
("PVTI_fresh", Some("I_fresh"), fresh.clone()),
("PVTI_nobodys", None, "AI Orchestrator plan".to_owned()),
],
vec![
their_label.clone(),
my_label.clone(),
stale_label.clone(),
fresh_label.clone(),
"bug".to_owned(),
],
vec![],
vec![],
vec![],
);
let _in_flight = session_in_flight();
journey::sweep_orphans(TOKEN, BOARD, REPOSITORY, &sweep_at(NOW))
.await
.expect("an orphan sweep over a board it can read succeeds");
let (titles, labels) = drive.left();
assert_eq!(
titles,
sorted(vec![
theirs.clone(),
mine,
fresh,
"AI Orchestrator plan".to_owned()
]),
"the sweep took an artifact a live run owns"
);
assert_eq!(
labels,
sorted(vec![
their_label.clone(),
my_label,
fresh_label,
"bug".to_owned()
]),
"the label sweep took a label a live run owns"
);
assert!(
!titles.contains(&stale) && !labels.contains(&stale_label),
"the sweep left the residue of a run that has ended, which is what it exists to recover"
);
beside.end();
journey::sweep_orphans(TOKEN, BOARD, REPOSITORY, &sweep_at(NOW))
.await
.expect("a second sweep over the same board succeeds");
let (titles, labels) = drive.left();
assert!(
!titles.contains(&theirs) && !labels.contains(&their_label),
"an ended run's artifacts were not recovered: {titles:?} {labels:?}"
);
}
#[tokio::test]
async fn a_process_id_reissued_to_a_new_run_costs_a_delay_and_never_a_deletion() {
let reused = ended_run(5);
let residue = artifact_title(reused, aged(2));
let residue_label = artifact_label(reused, aged(2));
let in_flight = artifact_title(reused, NOW);
let in_flight_label = artifact_label(reused, NOW);
let taken_over = Registration::take(&RUNS.registry, reused.process())
.expect("the run that was issued that number next");
let drive = Drive::plant(
vec![
("PVTI_residue", Some("I_residue"), residue.clone()),
("PVTI_in_flight", Some("I_in_flight"), in_flight.clone()),
],
vec![residue_label.clone(), in_flight_label.clone()],
vec![],
vec![],
vec![],
);
let _session = session_in_flight();
journey::sweep_orphans(TOKEN, BOARD, REPOSITORY, &sweep_at(NOW))
.await
.expect("an orphan sweep over a board it can read succeeds");
let (titles, labels) = drive.left();
assert_eq!(
titles,
sorted(vec![in_flight, residue.clone()]),
"the run holding that number now lost work, or the residue was taken while it runs"
);
assert_eq!(
labels,
sorted(vec![in_flight_label, residue_label.clone()]),
"the label sweep took a label while the run numbered like its writer is going"
);
drop(taken_over);
journey::sweep_orphans(TOKEN, BOARD, REPOSITORY, &sweep_at(NOW))
.await
.expect("a second sweep over the same board succeeds");
let (titles, labels) = drive.left();
assert!(
!titles.contains(&residue) && !labels.contains(&residue_label),
"the residue was not recovered once the run that took its number ended: {titles:?} {labels:?}"
);
}
#[tokio::test]
async fn an_artifact_another_deleter_took_first_leaves_the_cleanup_successful() {
let ended = ended_run(3);
let stale = artifact_title(ended, aged(2));
let stale_label = artifact_label(ended, aged(2));
let drive = Drive::plant(
vec![("PVTI_racing", Some("I_racing"), stale.clone())],
vec![stale_label.clone()],
vec!["PVTI_racing".to_owned()],
vec![stale_label.clone()],
vec![],
);
let _in_flight = session_in_flight();
journey::sweep_orphans(TOKEN, BOARD, REPOSITORY, &sweep_at(NOW))
.await
.expect("a delete of what has already gone is the outcome the delete was asking for");
let (titles, labels) = drive.left();
assert!(
titles.is_empty() && labels.is_empty(),
"{titles:?} {labels:?}"
);
assert_eq!(
sorted(drive.refused()),
sorted(vec!["PVTI_racing".to_owned(), stale_label]),
"the board did not refuse the deletes this case is about"
);
}
#[tokio::test]
async fn residue_a_delete_never_takes_fails_the_cleanup_rather_than_passing_quietly() {
let ended = ended_run(6);
let stuck = artifact_title(ended, aged(2));
let stuck_label = artifact_label(ended, aged(2));
let drive = Drive::plant(
vec![("PVTI_stuck", Some("I_stuck"), stuck.clone())],
vec![stuck_label.clone()],
vec![],
vec![],
vec!["PVTI_stuck".to_owned(), stuck_label.clone()],
);
let _in_flight = session_in_flight();
let refusal = journey::sweep_orphans(TOKEN, BOARD, REPOSITORY, &sweep_at(NOW))
.await
.expect_err("a sweep that left residue behind has not swept");
assert!(
refusal.contains("PVTI_stuck") && refusal.contains(&stuck_label),
"the refusal has to name what is still there, in both stores: {refusal}"
);
let (titles, labels) = drive.left();
assert_eq!(titles, vec![stuck]);
assert_eq!(labels, vec![stuck_label]);
}
#[tokio::test]
async fn this_runs_own_cleanup_removes_everything_it_wrote_and_nothing_else() {
let mine = artifact_title(RUNS.mine, NOW);
let also_mine = artifact_title(RUNS.mine, NOW + 1);
let my_label = artifact_label(RUNS.mine, NOW);
let ended = ended_run(4);
let theirs = artifact_title(ended, NOW);
let their_label = artifact_label(ended, NOW);
let drive = Drive::plant(
vec![
("PVTI_mine", Some("I_mine"), mine),
("PVTI_mine_draft", None, also_mine),
("PVTI_theirs", Some("I_theirs"), theirs.clone()),
],
vec![my_label, their_label.clone(), "bug".to_owned()],
vec![],
vec![],
vec![],
);
let _in_flight = session_in_flight();
journey::remove_live_state(TOKEN, BOARD, REPOSITORY, RUNS.mine, false)
.await
.expect("this run's own cleanup over a board it can read succeeds");
let (titles, labels) = drive.left();
assert_eq!(titles, vec![theirs]);
assert_eq!(labels, sorted(vec![their_label, "bug".to_owned()]));
assert!(
!drive.holds_issue("I_mine"),
"the issue behind this run's board item is still in the repository"
);
assert!(
drive.holds_issue("I_theirs"),
"this run's cleanup took another run's issue with it"
);
}
fn sorted(mut names: Vec<String>) -> Vec<String> {
names.sort();
names
}
fn serve(board: Arc<Mutex<Board>>) -> String {
let listener = TcpListener::bind("127.0.0.1:0").expect("a stand-in listener");
let host = format!("http://{}", listener.local_addr().unwrap());
thread::spawn(move || {
for stream in listener.incoming() {
let mut stream = stream.expect("a stand-in connection");
let (method, path, body) = read_request(&mut stream);
let (status, payload) = if method == "POST" && path == "/graphql" {
graphql(&board, &body.expect("a GraphQL document"))
} else {
rest(&board, &method, &path)
};
let response = format!(
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\n\
x-ratelimit-limit: 5000\r\nx-ratelimit-used: 1\r\n\
x-ratelimit-remaining: 4999\r\nx-ratelimit-resource: core\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n{payload}",
payload.len()
);
let _ = stream.write_all(response.as_bytes());
}
});
host
}
fn graphql(board: &Arc<Mutex<Board>>, request: &Value) -> (&'static str, String) {
let query = request["query"].as_str().expect("a GraphQL document");
let input = request
.pointer("/variables/input")
.cloned()
.unwrap_or(Value::Null);
let mut board = board
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if query.contains("on ProjectV2{items(first:100,after:$after)") {
assert_eq!(
request.pointer("/variables/id").and_then(Value::as_str),
Some(BOARD)
);
let nodes = board
.items
.iter()
.map(|(item, issue, title)| match issue {
Some(issue) => json!({"id":item,
"content":{"__typename":"Issue","id":issue,"title":title}}),
None => json!({"id":item,"content":{"title":title}}),
})
.collect::<Vec<_>>();
board.let_the_item_race_happen();
return answered(json!({"node":{"items":{"nodes":nodes,
"pageInfo":{"hasNextPage":false,"endCursor":null}}}}));
}
if query.contains("deleteProjectV2Item(input:$input)") {
let item = input["itemId"]
.as_str()
.expect("a board item id")
.to_owned();
if board.immortal(&item) {
board.refused.push(item.clone());
return (
"200 OK",
json!({"errors":[{"message":format!("this board will not part with {item}")}]})
.to_string(),
);
}
if !board.holds_item(&item) {
board.refused.push(item.clone());
return gone(&item);
}
board.items.retain(|(held, _, _)| *held != item);
return answered(json!({"deleteProjectV2Item":{"deletedItemId":item}}));
}
if query.contains("deleteIssue(input:$input)") {
let issue = input["issueId"].as_str().expect("an issue id").to_owned();
if !board.holds_issue(&issue) {
board.refused.push(issue.clone());
return gone(&issue);
}
board.issues.retain(|held| *held != issue);
return answered(json!({"deleteIssue":{"repository":{"id":"REPO_1"}}}));
}
(
"400 Bad Request",
json!({"errors":[{"message":format!("this stand-in answers no such document: {query}")}]})
.to_string(),
)
}
fn rest(board: &Arc<Mutex<Board>>, method: &str, path: &str) -> (&'static str, String) {
let mut board = board
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let listing = format!("/repos/{REPOSITORY}/labels");
if method == "GET" && path.starts_with(&format!("{listing}?")) {
let names = board
.labels
.iter()
.map(|name| json!({"name":name}))
.collect::<Vec<_>>();
board.let_the_label_race_happen();
return ("200 OK", Value::Array(names).to_string());
}
if method == "DELETE"
&& let Some(name) = path.strip_prefix(&format!("{listing}/"))
{
if board.immortal(name) {
board.refused.push(name.to_owned());
return (
"500 Internal Server Error",
json!({"message":"this repository will not part with that label"}).to_string(),
);
}
if !board.holds_label(name) {
board.refused.push(name.to_owned());
return (
"404 Not Found",
json!({"message":"Not Found",
"documentation_url":"https://docs.github.com/rest/issues/labels"})
.to_string(),
);
}
board.labels.retain(|held| held != name);
return ("204 No Content", String::new());
}
(
"404 Not Found",
json!({"message":format!("this stand-in answers no such endpoint: {method} {path}")})
.to_string(),
)
}
fn answered(data: Value) -> (&'static str, String) {
("200 OK", json!({ "data": data }).to_string())
}
fn gone(id: &str) -> (&'static str, String) {
(
"200 OK",
json!({"data":{},"errors":[{"type":"NOT_FOUND",
"message":format!("Could not resolve to a node with the global id of '{id}'.")}]})
.to_string(),
)
}
fn read_request(stream: &mut impl Read) -> (String, String, Option<Value>) {
let mut bytes = Vec::new();
let mut chunk = [0_u8; 4096];
loop {
let count = stream.read(&mut chunk).expect("a stand-in request");
assert!(count > 0, "the request ended before its headers");
bytes.extend_from_slice(&chunk[..count]);
if bytes.windows(4).any(|window| window == b"\r\n\r\n") {
break;
}
}
let header_end = bytes
.windows(4)
.position(|window| window == b"\r\n\r\n")
.expect("a header terminator")
+ 4;
let headers = String::from_utf8_lossy(&bytes[..header_end]).into_owned();
assert!(
headers.contains(&format!("authorization: Bearer {TOKEN}")),
"{headers}"
);
let mut request = headers.lines().next().unwrap_or_default().split(' ');
let method = request.next().unwrap_or_default().to_owned();
let path = request.next().unwrap_or_default().to_owned();
let length = headers
.lines()
.find_map(|line| {
line.to_ascii_lowercase()
.strip_prefix("content-length: ")
.and_then(|value| value.trim().parse::<usize>().ok())
})
.unwrap_or_default();
while bytes.len() - header_end < length {
let count = stream.read(&mut chunk).expect("a stand-in request body");
assert!(count > 0, "the request ended before its declared body");
bytes.extend_from_slice(&chunk[..count]);
}
let body = (length > 0).then(|| {
serde_json::from_slice(&bytes[header_end..header_end + length]).expect("request JSON")
});
(method, path, body)
}