mirror of
https://github.com/logos-blockchain/logos-blockchain-testing.git
synced 2026-08-07 23:33:11 +00:00
fix(tf): harden async binary resolution
This commit is contained in:
parent
9ecca30ef3
commit
12d604bd32
@ -86,7 +86,8 @@ impl BuildBinaryProvider {
|
|||||||
let mut command = Command::new(&self.command.program);
|
let mut command = Command::new(&self.command.program);
|
||||||
command
|
command
|
||||||
.current_dir(self.workspace_dir())
|
.current_dir(self.workspace_dir())
|
||||||
.args(&self.command.args);
|
.args(&self.command.args)
|
||||||
|
.kill_on_drop(true);
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
#[cfg(unix)]
|
||||||
|
use std::time::Instant;
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::{Read as _, Write as _},
|
io::{Read as _, Write as _},
|
||||||
@ -8,6 +10,7 @@ use std::{
|
|||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use sha2::{Digest as _, Sha256};
|
use sha2::{Digest as _, Sha256};
|
||||||
@ -160,6 +163,34 @@ async fn fails_when_build_command_does_not_create_output() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cancelling_build_resolution_stops_the_build_process() {
|
||||||
|
let temp = TempDir::new().expect("temp dir");
|
||||||
|
let output = temp.path().join("built-node");
|
||||||
|
let pid_file = temp.path().join("build.pid");
|
||||||
|
|
||||||
|
let provider = BuildBinaryProvider {
|
||||||
|
command: BuildCommand::new("sh")
|
||||||
|
.with_args(["-c", "printf '%s' \"$$\" > build.pid; exec sleep 60"]),
|
||||||
|
output_path: output,
|
||||||
|
working_dir: Some(temp.path().to_owned()),
|
||||||
|
lock_dir: Some(temp.path().join("locks")),
|
||||||
|
};
|
||||||
|
let resolution = tokio::spawn(async move { provider.resolve().await });
|
||||||
|
let pid = wait_for_pid(&pid_file).await;
|
||||||
|
|
||||||
|
resolution.abort();
|
||||||
|
let error = resolution
|
||||||
|
.await
|
||||||
|
.expect_err("resolution should be cancelled");
|
||||||
|
assert!(error.is_cancelled());
|
||||||
|
assert!(
|
||||||
|
wait_until_process_exits(pid, Duration::from_secs(2)),
|
||||||
|
"build process {pid} should exit when resolution is cancelled"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn downloads_binary_from_minimal_http_server() {
|
async fn downloads_binary_from_minimal_http_server() {
|
||||||
let temp = TempDir::new().expect("temp dir");
|
let temp = TempDir::new().expect("temp dir");
|
||||||
@ -180,6 +211,53 @@ async fn downloads_binary_from_minimal_http_server() {
|
|||||||
assert_eq!(fs::read(path).expect("downloaded file"), body);
|
assert_eq!(fs::read(path).expect("downloaded file"), body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn concurrent_downloads_share_one_cached_artifact() {
|
||||||
|
let temp = TempDir::new().expect("temp dir");
|
||||||
|
let body = b"downloaded-node";
|
||||||
|
|
||||||
|
let mut server = SingleResponseServer::start_paused(body);
|
||||||
|
let url = server.url();
|
||||||
|
let cache_dir = temp.path().join("cache");
|
||||||
|
let checksum = sha256_hex(body);
|
||||||
|
|
||||||
|
let first_provider = DownloadBinaryProvider {
|
||||||
|
url: DownloadUrl::Fixed(url.clone()),
|
||||||
|
sha256: Some(DownloadChecksum::Fixed(checksum.clone())),
|
||||||
|
cache_dir: Some(cache_dir.clone()),
|
||||||
|
processor: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let second_provider = DownloadBinaryProvider {
|
||||||
|
url: DownloadUrl::Fixed(url),
|
||||||
|
sha256: Some(DownloadChecksum::Fixed(checksum)),
|
||||||
|
cache_dir: Some(cache_dir),
|
||||||
|
processor: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let first = tokio::spawn(async move { first_provider.resolve().await });
|
||||||
|
server.wait_for_request().await;
|
||||||
|
|
||||||
|
let second = tokio::spawn(async move { second_provider.resolve().await });
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
assert!(!second.is_finished(), "second resolution should wait");
|
||||||
|
|
||||||
|
server.release_response();
|
||||||
|
let (first, second) = tokio::time::timeout(Duration::from_secs(2), async {
|
||||||
|
(first.await, second.await)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("concurrent resolutions timed out");
|
||||||
|
|
||||||
|
let first = first.expect("first task failed").expect("first resolution");
|
||||||
|
let second = second
|
||||||
|
.expect("second task failed")
|
||||||
|
.expect("second resolution");
|
||||||
|
|
||||||
|
assert_eq!(first, second);
|
||||||
|
assert_eq!(fs::read(first).expect("downloaded file"), body);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rejects_download_checksum_mismatch() {
|
async fn rejects_download_checksum_mismatch() {
|
||||||
let temp = TempDir::new().expect("temp dir");
|
let temp = TempDir::new().expect("temp dir");
|
||||||
@ -265,6 +343,43 @@ fn write_file(path: &Path, contents: &[u8]) {
|
|||||||
fs::write(path, contents).expect("write file");
|
fs::write(path, contents).expect("write file");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn wait_for_pid(path: &Path) -> u32 {
|
||||||
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
|
||||||
|
loop {
|
||||||
|
if let Ok(pid) = fs::read_to_string(path) {
|
||||||
|
return pid.parse().expect("valid process id");
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
tokio::time::Instant::now() < deadline,
|
||||||
|
"build process did not publish its id"
|
||||||
|
);
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn wait_until_process_exits(pid: u32, timeout: Duration) -> bool {
|
||||||
|
let deadline = Instant::now() + timeout;
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
if !process_exists(pid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
!process_exists(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn process_exists(pid: u32) -> bool {
|
||||||
|
std::process::Command::new("kill")
|
||||||
|
.arg("-0")
|
||||||
|
.arg(pid.to_string())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.status()
|
||||||
|
.is_ok_and(|status| status.success())
|
||||||
|
}
|
||||||
|
|
||||||
fn missing_binary_provider(path: PathBuf) -> BinaryProviderRef {
|
fn missing_binary_provider(path: PathBuf) -> BinaryProviderRef {
|
||||||
Arc::new(PathBinaryProvider::new(path))
|
Arc::new(PathBinaryProvider::new(path))
|
||||||
}
|
}
|
||||||
@ -309,17 +424,29 @@ fn sha256_hex(bytes: &[u8]) -> String {
|
|||||||
|
|
||||||
struct SingleResponseServer {
|
struct SingleResponseServer {
|
||||||
addr: String,
|
addr: String,
|
||||||
|
request_started: Option<tokio::sync::oneshot::Receiver<()>>,
|
||||||
|
release_response: Option<std::sync::mpsc::Sender<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SingleResponseServer {
|
impl SingleResponseServer {
|
||||||
fn start(body: &'static [u8]) -> Self {
|
fn start(body: &'static [u8]) -> Self {
|
||||||
|
let mut server = Self::start_paused(body);
|
||||||
|
server.release_response();
|
||||||
|
server
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_paused(body: &'static [u8]) -> Self {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test http server");
|
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test http server");
|
||||||
let addr = listener.local_addr().expect("server addr").to_string();
|
let addr = listener.local_addr().expect("server addr").to_string();
|
||||||
|
let (request_started_tx, request_started_rx) = tokio::sync::oneshot::channel();
|
||||||
|
let (release_response_tx, release_response_rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let (mut stream, _) = listener.accept().expect("accept one request");
|
let (mut stream, _) = listener.accept().expect("accept one request");
|
||||||
let mut buffer = [0; 1024];
|
let mut buffer = [0; 1024];
|
||||||
let _ = stream.read(&mut buffer);
|
let _ = stream.read(&mut buffer);
|
||||||
|
let _ = request_started_tx.send(());
|
||||||
|
release_response_rx.recv().expect("release response");
|
||||||
let response = format!(
|
let response = format!(
|
||||||
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||||
body.len()
|
body.len()
|
||||||
@ -331,10 +458,30 @@ impl SingleResponseServer {
|
|||||||
stream.write_all(body).expect("write body");
|
stream.write_all(body).expect("write body");
|
||||||
});
|
});
|
||||||
|
|
||||||
Self { addr }
|
Self {
|
||||||
|
addr,
|
||||||
|
request_started: Some(request_started_rx),
|
||||||
|
release_response: Some(release_response_tx),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url(&self) -> String {
|
fn url(&self) -> String {
|
||||||
format!("http://{}/binary", self.addr)
|
format!("http://{}/binary", self.addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn wait_for_request(&mut self) {
|
||||||
|
self.request_started
|
||||||
|
.take()
|
||||||
|
.expect("request receiver")
|
||||||
|
.await
|
||||||
|
.expect("request started");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_response(&mut self) {
|
||||||
|
self.release_response
|
||||||
|
.take()
|
||||||
|
.expect("response sender")
|
||||||
|
.send(())
|
||||||
|
.expect("release response");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user