Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/runtime-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ pub const CANDIDATE_VERSION: &str = "5.0.0-dev.6";
mod error;
mod error_stop_diagnostics;
pub use error::CandidateError;
#[cfg(test)]
mod test_scratch;
#[cfg(test)]
pub(crate) use test_scratch::next_test_scratch;

#[derive(Debug, Serialize)]
pub struct Candidate {
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/project/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,12 +525,12 @@ mod tests {
#[test]
fn source_replacement_after_inventory_refuses_capture() {
let root = Fixture(std::env::temp_dir().canonicalize().unwrap().join(format!(
"hack-snapshot-replacement-{}-{}",
"hack-snapshot-replacement-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
.as_nanos(), crate::next_test_scratch()
)));
fs::create_dir(&root.0).unwrap();
let project = root.0.join("project");
Expand Down
5 changes: 3 additions & 2 deletions packages/runtime-core/src/provider/config_audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,13 @@ mod tests {
#[test]
fn an_interrupted_create_is_recognized_only_by_this_pools_own_record() {
let checkout = std::env::temp_dir().canonicalize().unwrap().join(format!(
"hack-config-audit-recorded-{}-{}",
"hack-config-audit-recorded-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
.as_nanos(),
crate::next_test_scratch()
));
fs::create_dir(&checkout).unwrap();
let candidate = Candidate::discover(&checkout).unwrap();
Expand Down
5 changes: 3 additions & 2 deletions packages/runtime-core/src/provider/dependency_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,13 @@ mod tests {
impl Fixture {
fn new() -> Self {
let root = Path::new("/tmp").canonicalize().unwrap().join(format!(
"hkd-{}-{}",
"hkd-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
.as_nanos(),
crate::next_test_scratch()
));
state::private_directory(&root).unwrap();
Self(root)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use super::*;
use std::{
cell::Cell,
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::{ffi::OsStrExt, fs::PermissionsExt, net::UnixListener},
fd::OwnedFd,
unix::{fs::PermissionsExt, net::UnixListener},
},
sync::atomic::{AtomicU64, Ordering},
};
Expand Down Expand Up @@ -48,36 +48,8 @@ impl Fixture {
fn unlistened(&self, slot: u8) -> OwnedFd {
// A stale fixture needs a bound stream inode, never a transient listener.
// It stays unlistened even if a concurrent child inherits a descriptor.
let fd = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_STREAM, 0) };
assert!(fd >= 0);
let socket = unsafe { OwnedFd::from_raw_fd(fd) };
assert_eq!(
unsafe { libc::fcntl(socket.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) },
0
);
let target = path(&self.0, slot);
let bytes = target.as_os_str().as_bytes();
let mut address: libc::sockaddr_un = unsafe { std::mem::zeroed() };
address.sun_family = libc::AF_UNIX as libc::sa_family_t;
assert!(!bytes.contains(&0) && bytes.len() < address.sun_path.len());
for (destination, byte) in address.sun_path.iter_mut().zip(bytes) {
*destination = *byte as libc::c_char;
}
let length = std::mem::offset_of!(libc::sockaddr_un, sun_path) + bytes.len() + 1;
#[cfg(target_vendor = "apple")]
{
address.sun_len = length.try_into().unwrap();
}
assert_eq!(
unsafe {
libc::bind(
socket.as_raw_fd(),
(&raw const address).cast(),
length.try_into().unwrap(),
)
},
0
);
let socket = crate::provider::test_socket::bound_unlistened(&target);
fs::set_permissions(target, fs::Permissions::from_mode(0o600)).unwrap();
socket
}
Expand Down Expand Up @@ -115,8 +87,28 @@ fn stale_stream_inode_never_listens_even_while_an_owned_duplicate_survives() {
assert_eq!(observed(&fixture.0, 0).unwrap(), Some(selected));
}

/// Run one ignored fixture of this module alone in a child test process.
///
/// The selection must find a listener this test just dropped closed. In the shared parallel
/// test process an unrelated test's pre-exec fork (`process::clean_command`) holds a copy of
/// every descriptor open at the fork until it execs, so the dropped listener can still accept
/// and the selection correctly refuses it as live.
fn isolated(child: &str) {
use crate::provider::test_executable::{ISOLATED_HANG_GUARD, run_isolated};
run_isolated(
&format!("provider::dependency_socket::quiescent_recovery::tests::{child}"),
ISOLATED_HANG_GUARD,
);
}

#[test]
fn first_socket_refusal_records_closed_live_listener_facts_without_removal() {
isolated("closed_live_listener_facts_child");
}

#[test]
#[ignore = "isolated dropped-listener fixture launched by first_socket_refusal_records_closed_live_listener_facts_without_removal"]
fn closed_live_listener_facts_child() {
use super::super::recovery::observation_diagnostic as diagnostic;
let fixture = Fixture::new();
let listener = fixture.bind(0);
Expand Down Expand Up @@ -248,6 +240,12 @@ fn owner_graph_boot_or_volume_proof_drift_refuses_before_any_effect() {

#[test]
fn live_listener_and_foreign_replacement_are_never_removed() {
isolated("live_listener_and_foreign_replacement_child");
}

#[test]
#[ignore = "isolated dropped-listener fixture launched by live_listener_and_foreign_replacement_are_never_removed"]
fn live_listener_and_foreign_replacement_child() {
let fixture = Fixture::new();
let listener = fixture.bind(0);
assert!(current(fixture.scope(), &fixture.0).is_err());
Expand Down
5 changes: 3 additions & 2 deletions packages/runtime-core/src/provider/disk_audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,13 @@ mod tests {
}
}
let root = std::env::temp_dir().join(format!(
"hack-audit-{}-{}",
"hack-audit-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
.as_nanos(),
crate::next_test_scratch()
));
std::fs::create_dir(&root).unwrap();
let root = root.canonicalize().unwrap();
Expand Down
5 changes: 3 additions & 2 deletions packages/runtime-core/src/provider/disk_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,13 @@ mod tests {
impl Fixture {
fn new(label: &str) -> Self {
let root = std::env::temp_dir().join(format!(
"hack-disk-template-{label}-{}-{}",
"hack-disk-template-{label}-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
.as_nanos(),
crate::next_test_scratch()
));
fs::create_dir(&root).unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).unwrap();
Expand Down
5 changes: 3 additions & 2 deletions packages/runtime-core/src/provider/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,12 +715,13 @@ mod tests {
fn bounded_acquisition_cancellation_and_expiry_do_not_initialize_state() {
use std::os::unix::fs::DirBuilderExt;
let path = std::env::temp_dir().join(format!(
"hack-engine-acquire-{}-{}",
"hack-engine-acquire-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
.as_nanos(),
crate::next_test_scratch()
));
std::fs::DirBuilder::new()
.mode(0o700)
Expand Down
8 changes: 8 additions & 0 deletions packages/runtime-core/src/provider/engine/relay_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@ mod tests {
}
let (client, mut server) = UnixStream::pair().unwrap();
server.write_all(&[1, 0, 0, 0, 0, 0, 0, 5, b'x']).unwrap();
// Shut the peer down instead of only dropping it: an unrelated parallel test's
// pre-exec fork (`process::clean_command`) can hold a copy of this descriptor until
// it execs, and the client then sees no end of stream yet. A shutdown ends the
// stream for every copy, so the truncated frame is always observed at its close.
// `inherited` models such a copy, so this holds whether or not a fork is running.
let inherited = server.try_clone().unwrap();
server.shutdown(std::net::Shutdown::Write).unwrap();
drop(server);
client.set_nonblocking(true).unwrap();
let mut transport = RelayExec {
Expand All @@ -329,6 +336,7 @@ mod tests {
received: 0,
};
assert!(transport.poll(&mut vec![], &mut vec![]).is_err());
drop(inherited);
let mut out = vec![0; 4096];
assert!(decode(&mut frame(2, b"x"), &mut out, &mut vec![]).is_err());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,20 @@ pub fn retire(candidate: &Candidate, run: &str, expected: &str) -> Result<Value,
mod tests {
use super::*;

/// Run one ignored dead-publisher fixture of this module alone in a child test process.
///
/// [`abandoned`] drops a live publication and requires its listener closed. In the shared
/// parallel test process an unrelated test's pre-exec fork (`process::clean_command`) holds
/// a copy of every descriptor open at the fork until it execs, so the dropped listener can
/// still accept and the dead-publisher proof correctly refuses. Every test here uses it.
fn isolated(child: &str) {
use crate::provider::test_executable::{ISOLATED_HANG_GUARD, run_isolated};
run_isolated(
&format!("provider::graph::acknowledged_publisher::missing_lock::tests::{child}"),
ISOLATED_HANG_GUARD,
);
}

fn abandoned() -> (
super::super::super::tests::Fixture,
Candidate,
Expand Down Expand Up @@ -854,6 +868,12 @@ mod tests {

#[test]
fn missing_lock_intent_blocks_ordinary_publication_until_exact_archival_completion() {
isolated("intent_blocks_publication_child");
}

#[test]
#[ignore = "isolated dead-publisher fixture launched by missing_lock_intent_blocks_ordinary_publication_until_exact_archival_completion"]
fn intent_blocks_publication_child() {
let (_fixture, candidate, run, root, intent) = abandoned();
state::write(&intent_path(&root), &intent).unwrap();
assert!(require_no_pending(&candidate, &run).is_err());
Expand Down Expand Up @@ -918,6 +938,12 @@ mod tests {

#[test]
fn replacement_lock_recovers_exact_temp_and_hardlink_crashes() {
isolated("temp_and_hardlink_crashes_child");
}

#[test]
#[ignore = "isolated dead-publisher fixture launched by replacement_lock_recovers_exact_temp_and_hardlink_crashes"]
fn temp_and_hardlink_crashes_child() {
for stage in ["temp", "linked", "final"] {
let (_fixture, candidate, run, root, intent) = abandoned();
state::write(&intent_path(&root), &intent).unwrap();
Expand Down Expand Up @@ -962,6 +988,12 @@ mod tests {

#[test]
fn replacement_lock_preserves_foreign_names_and_malformed_intent() {
isolated("foreign_names_child");
}

#[test]
#[ignore = "isolated dead-publisher fixture launched by replacement_lock_preserves_foreign_names_and_malformed_intent"]
fn foreign_names_child() {
let (_fixture, candidate, run, root, mut intent) = abandoned();
let temp = root.join(temp_name(&intent.selection_sha256));
std::os::unix::fs::symlink("/tmp/foreign", &temp).unwrap();
Expand Down Expand Up @@ -994,6 +1026,12 @@ mod tests {

#[test]
fn initial_selection_refuses_foreign_completion_pending_without_effects() {
isolated("foreign_completion_pending_child");
}

#[test]
#[ignore = "isolated dead-publisher fixture launched by initial_selection_refuses_foreign_completion_pending_without_effects"]
fn foreign_completion_pending_child() {
let (_fixture, _candidate, _run, root, _intent) = abandoned();
let pending = completion_path(&root).with_extension("pending");
let original_owner = fs::read(root.join("owner.json")).unwrap();
Expand All @@ -1019,6 +1057,12 @@ mod tests {

#[test]
fn replacement_lock_path_substitution_refuses_with_both_inodes_preserved() {
isolated("lock_path_substitution_child");
}

#[test]
#[ignore = "isolated dead-publisher fixture launched by replacement_lock_path_substitution_refuses_with_both_inodes_preserved"]
fn lock_path_substitution_child() {
let (_fixture, _candidate, _run, root, intent) = abandoned();
state::write(&intent_path(&root), &intent).unwrap();
let held = replacement_lock(&root, &intent).unwrap();
Expand Down
23 changes: 23 additions & 0 deletions packages/runtime-core/src/provider/graph/dependency_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -702,8 +702,26 @@ mod tests {
record.sockets.insert(slot, (m.dev(), m.ino()));
listener
}
/// Run one ignored socket-recovery fixture of this module alone in a child test process.
///
/// Recovery must find a listener this test just dropped closed. In the shared parallel
/// test process an unrelated test's pre-exec fork (`process::clean_command`) holds a copy
/// of every descriptor open at the fork until it execs, so the dropped listener can still
/// accept and recovery correctly refuses it as live.
fn isolated(child: &str) {
use crate::provider::test_executable::{ISOLATED_HANG_GUARD, run_isolated};
run_isolated(
&format!("provider::graph::dependency_slots::tests::{child}"),
ISOLATED_HANG_GUARD,
);
}
#[test]
fn recovery_refuses_live_and_replaced_sockets_without_touching_siblings() {
isolated("live_and_replaced_sockets_child");
}
#[test]
#[ignore = "isolated socket-recovery fixture launched by recovery_refuses_live_and_replaced_sockets_without_touching_siblings"]
fn live_and_replaced_sockets_child() {
let fixture = ShortFixture::new();
let mut a = fixture_record('1', 0);
let mut b = fixture_record('2', 1);
Expand All @@ -723,6 +741,11 @@ mod tests {
}
#[test]
fn recovery_preflights_entire_assignment_before_unlinking() {
isolated("preflight_assignment_child");
}
#[test]
#[ignore = "isolated socket-recovery fixture launched by recovery_preflights_entire_assignment_before_unlinking"]
fn preflight_assignment_child() {
let fixture = ShortFixture::new();
let mut a = fixture_record('1', 0);
a.slots.insert(1, 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,16 @@ mod tests {
sockets: BTreeMap::new(),
};
for slot in 0..3 {
// Stale sockets that never listened: a dropped listener could still accept
// through an unrelated parallel test's pre-exec fork copy.
let path = socket(&short_home, slot);
let listener = UnixListener::bind(&path).unwrap();
let stale = crate::provider::test_socket::bound_unlistened(&path);
fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).unwrap();
let m = fs::symlink_metadata(&path).unwrap();
if slot < 2 {
record.sockets.insert(slot, (m.dev(), m.ino()));
}
drop(listener);
drop(stale);
}
state::private_directory(&super::super::root(&candidate)).unwrap();
state::write(
Expand Down
Loading
Loading