Skip to content

Commit b7ff10e

Browse files
committed
fix: fix onhost supervisor reloading test
1 parent 7a9f99e commit b7ff10e

3 files changed

Lines changed: 70 additions & 33 deletions

File tree

agent-control/tests/on_host/scenarios/file_logging.rs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use std::{fs, io, path::Path, time::Duration};
22

33
use crate::{
44
common::{
5-
agent_control::start_agent_control_with_custom_config, base_paths::TempBasePaths,
6-
retry::retry, runtime::tokio_runtime,
5+
agent_control::{StartedAgentControl, start_agent_control_with_custom_config},
6+
base_paths::TempBasePaths,
7+
retry::retry,
8+
runtime::tokio_runtime,
79
},
810
on_host::tools::{
911
config::{OnHostAgentControlConfigBuilder, create_file, create_local_config},
@@ -123,7 +125,7 @@ fn run_file_logging_scenario(
123125
initial_message: &str,
124126
reload_file_logging: bool,
125127
reload_message: &str,
126-
) -> TempBasePaths {
128+
) -> (TempBasePaths, StartedAgentControl) {
127129
let mut opamp_server = FakeServer::start(tokio_runtime().handle());
128130

129131
let dirs = TempBasePaths::default();
@@ -154,7 +156,7 @@ fn run_file_logging_scenario(
154156
dirs.local_dir(),
155157
);
156158

157-
let _agent_control =
159+
let agent_control =
158160
start_agent_control_with_custom_config(dirs.base_paths(), AGENT_CONTROL_MODE_ON_HOST);
159161

160162
let sub_agent_instance_id =
@@ -172,7 +174,8 @@ fn run_file_logging_scenario(
172174
// Give some time for the echo output to be captured in the log files
173175
std::thread::sleep(Duration::from_secs(5));
174176

175-
dirs
177+
// Return the handle so AC keeps running while the caller waits for the post-reload logs.
178+
(dirs, agent_control)
176179
}
177180

178181
/// File logging enable/disable combinations with before and after reload checks
@@ -188,7 +191,8 @@ fn test_file_logging_reload(
188191
) {
189192
let agent_id = format!("file-logging-agent-{first_run_enabled}-{second_run_enabled}");
190193

191-
let dirs = run_file_logging_scenario(
194+
// Keep the handle alive so AC keeps running during the wait below.
195+
let (dirs, _agent_control) = run_file_logging_scenario(
192196
&agent_id,
193197
first_run_enabled,
194198
first_run_message,
@@ -197,24 +201,26 @@ fn test_file_logging_reload(
197201
);
198202

199203
let log_dir_path = dirs.log_dir();
200-
let agent_logs_dir = log_dir_path.join(&agent_id);
201-
assert!(
202-
agent_logs_dir.exists(),
203-
"Log directory {agent_logs_dir:?} does not exist"
204-
);
205204

205+
// Wait (AC still running) until the logs match the expected end state after the reload.
206206
let all_contents = retry(60, Duration::from_secs(1), || {
207-
Ok(collect_stdout_logs(&log_dir_path, &agent_id)?)
207+
let contents = collect_stdout_logs(&log_dir_path, &agent_id)?;
208+
if first_run_enabled == contents.contains(first_run_message)
209+
&& second_run_enabled == contents.contains(second_run_message)
210+
{
211+
Ok(contents)
212+
} else {
213+
Err(format!("log state not settled yet. Log contents: {contents}").into())
214+
}
208215
});
209216

210-
// If the logs are enabled for the run the string must be found, same for disabled and not found
211217
assert!(
212218
first_run_enabled == all_contents.contains(first_run_message),
213-
"First run log not found (pre-reload). Log contents: {all_contents}"
219+
"First run log mismatch (pre-reload). Log contents: {all_contents}"
214220
);
215221
assert!(
216222
second_run_enabled == all_contents.contains(second_run_message),
217-
"Second run log not found (post-reload). Log contents: {all_contents}"
223+
"Second run log mismatch (post-reload). Log contents: {all_contents}"
218224
);
219225
}
220226

@@ -226,7 +232,8 @@ fn onhost_supervisor_reloading_keeps_file_logging_disabled() {
226232
let unique_str_2 = "keeps_disabled_run2";
227233
let agent_id = "test-agent-logs-always-disabled";
228234

229-
let dirs = run_file_logging_scenario(agent_id, false, unique_str_1, false, unique_str_2);
235+
let (dirs, _agent_control) =
236+
run_file_logging_scenario(agent_id, false, unique_str_1, false, unique_str_2);
230237

231238
let log_dir_path = dirs.log_dir();
232239
let agent_logs_dir = log_dir_path.join(agent_id);

self-replacer/tests/integration_tests.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use tempfile::TempDir;
1010

1111
mod test_helpers;
1212
use self_replacer::{BinaryReplacer, SelfReplacer};
13-
use test_helpers::{copy_example_binary, create_modified_binary};
13+
use test_helpers::{copy_example_binary, create_modified_binary, exec_serial_guard};
1414

1515
use self_replacer::BACKUP_SUFFIX;
1616

@@ -20,6 +20,9 @@ use self_replacer::BACKUP_SUFFIX;
2020

2121
#[test]
2222
fn test_self_replacement_with_real_binary() {
23+
// Serialize with the other write+exec test to avoid ETXTBSY (see exec_serial_guard).
24+
let _guard = exec_serial_guard();
25+
2326
let temp_dir = TempDir::new().unwrap();
2427
let test_dir = temp_dir.path();
2528

@@ -130,6 +133,9 @@ mod unix_specific {
130133

131134
#[test]
132135
fn test_permission_preservation_with_real_binary() {
136+
// Serialize with the other write+exec test to avoid ETXTBSY (see exec_serial_guard).
137+
let _guard = exec_serial_guard();
138+
133139
let temp_dir = TempDir::new().unwrap();
134140
let test_dir = temp_dir.path();
135141

self-replacer/tests/test_helpers.rs

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22
33
use std::fs;
44
use std::path::{Path, PathBuf};
5+
use std::sync::{Mutex, Once};
6+
7+
/// Serializes the integration tests that write a binary and then execute it.
8+
///
9+
/// Running them concurrently races on Linux: while one test's `fs::copy`/`fs::write` holds a
10+
/// writable fd to a binary, another test spawning a process (`fork` + `exec`) inherits that fd, so
11+
/// exec'ing the just-written binary can fail with `ETXTBSY` ("Text file busy"). Holding this lock
12+
/// for the duration of each such test keeps only one write+exec sequence in flight at a time.
13+
static EXEC_SERIAL_LOCK: Mutex<()> = Mutex::new(());
14+
15+
/// Acquires the [`EXEC_SERIAL_LOCK`], recovering from poisoning (a failing test that panicked while
16+
/// holding it must not cascade-poison the others).
17+
pub fn exec_serial_guard() -> std::sync::MutexGuard<'static, ()> {
18+
EXEC_SERIAL_LOCK
19+
.lock()
20+
.unwrap_or_else(|poisoned| poisoned.into_inner())
21+
}
22+
23+
/// Ensures the example binary is built at most once, even when parallel tests call
24+
/// [`get_example_binary`] concurrently.
25+
static BUILD_EXAMPLE: Once = Once::new();
526

627
/// Returns path to the example binary, building it automatically if needed.
728
///
@@ -29,23 +50,26 @@ pub fn get_example_binary() -> PathBuf {
2950
};
3051
path.push(binary_name);
3152

32-
// Build the example if it doesn't exist
33-
if !path.exists() {
34-
eprintln!("Example binary not found, building it...");
35-
let output = std::process::Command::new("cargo")
36-
.arg("build")
37-
.arg("--example")
38-
.arg("self_replacing_binary")
39-
.output()
40-
.expect("Failed to run cargo build");
41-
42-
if !output.status.success() {
43-
panic!(
44-
"Failed to build example binary:\n{}",
45-
String::from_utf8_lossy(&output.stderr)
46-
);
53+
// Build the example if it doesn't exist, but only once across all (parallel) tests.
54+
let build_path = path.clone();
55+
BUILD_EXAMPLE.call_once(move || {
56+
if !build_path.exists() {
57+
eprintln!("Example binary not found, building it...");
58+
let output = std::process::Command::new("cargo")
59+
.arg("build")
60+
.arg("--example")
61+
.arg("self_replacing_binary")
62+
.output()
63+
.expect("Failed to run cargo build");
64+
65+
if !output.status.success() {
66+
panic!(
67+
"Failed to build example binary:\n{}",
68+
String::from_utf8_lossy(&output.stderr)
69+
);
70+
}
4771
}
48-
}
72+
});
4973

5074
assert!(
5175
path.exists(),

0 commit comments

Comments
 (0)