Skip to content

Commit dab99f5

Browse files
committed
fixed review comments H7-H12
1 parent 7a97c79 commit dab99f5

4 files changed

Lines changed: 120 additions & 90 deletions

File tree

feature_integration_tests/test_cases/daemon_helpers.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,14 @@ def find_binary_in_runfiles(target_path: str) -> Path | None:
105105
return None
106106

107107

108-
def get_binary_path(target: str, version: str = "rust") -> Path:
108+
def get_binary_path(target: str) -> Path:
109109
"""
110110
Get path to a binary, either from runfiles or by building it.
111111
112112
Parameters
113113
----------
114114
target : str
115115
Bazel target path.
116-
version : str
117-
Build version ("rust" or "cpp").
118-
119116
Returns
120117
-------
121118
Path
@@ -411,7 +408,7 @@ def launch_manager_daemon(
411408

412409
# Get launch_manager daemon binary (from runfiles or build it)
413410
daemon_target = "@score_lifecycle_health//src/launch_manager_daemon:launch_manager"
414-
daemon_binary = get_binary_path(daemon_target, version)
411+
daemon_binary = get_binary_path(daemon_target)
415412

416413
# Copy daemon to bin directory
417414
daemon_path = bin_dir / "launch_manager"
@@ -424,7 +421,7 @@ def launch_manager_daemon(
424421
("@score_lifecycle_health//examples/cpp_supervised_app:cpp_supervised_app", "cpp_supervised_app"),
425422
("@score_lifecycle_health//examples/control_application:control_daemon", "control_daemon"),
426423
]:
427-
app_binary = get_binary_path(app_target, version)
424+
app_binary = get_binary_path(app_target)
428425
app_dest = bin_dir / app_name
429426
shutil.copy2(app_binary, app_dest)
430427
app_dest.chmod(0o755)

feature_integration_tests/test_cases/tests/lifecycle/test_process_launching_with_daemon.py

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def setup_test_app(self, launch_manager_daemon: dict[str, Any], version: str) ->
9696
app_target = "@score_lifecycle_health//examples/cpp_supervised_app:cpp_supervised_app"
9797
app_name = "cpp_supervised_app"
9898

99-
app_binary = get_binary_path(app_target, version)
99+
app_binary = get_binary_path(app_target)
100100

101101
# Copy to daemon bin directory
102102
dest_path = bin_dir / app_name
@@ -150,16 +150,12 @@ def test_supervised_app_launches(
150150
)
151151
print(f"\nDaemon reported supervised process PID events: {', '.join(pid_events[:5])}")
152152

153-
# Verify daemon is still running
153+
# Verify daemon is still running and target app appears in supervision logs.
154154
assert daemon.is_running(), "Launch Manager daemon stopped unexpectedly"
155-
156-
# Check daemon logs for supervision activity
157155
logs = daemon.get_logs()
158-
print(f"\nDaemon logs:\n{logs}")
159-
160-
# Verify logs show component was started
161-
# (Actual log messages depend on Launch Manager implementation)
162-
assert len(logs) > 0, "No daemon logs generated"
156+
assert app_name in logs or result.returncode == 0, (
157+
f"No target-specific supervision evidence for {app_name}.\nDaemon logs:\n{logs}"
158+
)
163159

164160
def test_supervised_app_recovery(
165161
self, launch_manager_daemon: dict[str, Any], setup_test_app: Path, version: str
@@ -195,36 +191,40 @@ def test_supervised_app_recovery(
195191
print(f"\nKilling supervised app (PID: {pid})")
196192
subprocess.run(["kill", "-9", pid], check=True)
197193

198-
# Wait for daemon to detect failure and restart
199-
time.sleep(2.0)
200-
201-
# Verify app was restarted
202-
result = subprocess.run(
203-
["pgrep", "-f", app_name],
204-
capture_output=True,
205-
text=True,
206-
check=False,
207-
)
208-
209-
if result.returncode == 0:
210-
new_pid = result.stdout.strip().split("\n")[0]
194+
# Poll for restart to avoid fixed-sleep race conditions.
195+
deadline = time.time() + 12.0
196+
new_pid = None
197+
while time.time() < deadline:
198+
result = subprocess.run(
199+
["pgrep", "-f", app_name],
200+
capture_output=True,
201+
text=True,
202+
check=False,
203+
)
204+
if result.returncode == 0:
205+
candidates = [p for p in result.stdout.strip().split("\n") if p and p != pid]
206+
if candidates:
207+
new_pid = candidates[0]
208+
break
209+
time.sleep(0.25)
210+
211+
if new_pid is not None:
211212
print(f"App restarted with new PID: {new_pid}")
212-
assert new_pid != pid, "PID should be different after restart"
213213
else:
214214
logs = daemon.get_logs()
215215
pytest.fail(
216-
"Supervised app was not found after forced kill; expected daemon recovery restart."
217-
f"\nDaemon logs:\n{logs}"
216+
"Supervised app was not restarted within timeout after forced kill."
217+
f"\nTarget app: {app_name}\nDaemon logs:\n{logs}"
218218
)
219219
else:
220220
logs = daemon.get_logs()
221-
recovery_signals = [
222-
f"unexpected termination of process",
223-
"Activating Recovery state.",
224-
f"Got kRunning timeout for process",
221+
target_patterns = [
222+
rf"unexpected termination of process.*\(\s*{re.escape(app_name)}\s*\)",
223+
rf"Got kRunning timeout for process.*\(\s*{re.escape(app_name)}\s*\)",
225224
]
226-
assert any(signal in logs for signal in recovery_signals), (
227-
f"Supervised app {app_name} not running and no recovery diagnostics found.\nDaemon logs:\n{logs}"
225+
assert any(re.search(pattern, logs) for pattern in target_patterns), (
226+
f"Supervised app {app_name} not running and no target-specific recovery diagnostics found."
227+
f"\nDaemon logs:\n{logs}"
228228
)
229229

230230
except subprocess.CalledProcessError as e:
@@ -264,16 +264,42 @@ def test_watchdog_detection(self, launch_manager_daemon: dict[str, Any], version
264264
3. Validate recovery action is triggered
265265
"""
266266
daemon = launch_manager_daemon["daemon"]
267+
app_name = "rust_supervised_app" if version == "rust" else "cpp_supervised_app"
267268

268-
# Give daemon enough time to perform supervision cycles and emit diagnostics.
269-
time.sleep(2.0)
270-
logs = daemon.get_logs()
271-
272-
watchdog_like_signals = [
273-
"Got kRunning timeout for process",
274-
"Problem discovered in PG MainPG Activating Recovery state.",
275-
"unexpected termination of process",
276-
]
277-
assert any(signal in logs for signal in watchdog_like_signals), (
278-
"No supervision/watchdog-related diagnostics found in daemon logs."
269+
# Setup: stop the supervised process to emulate a non-reporting workload.
270+
result = subprocess.run(
271+
["pgrep", "-f", app_name],
272+
capture_output=True,
273+
text=True,
274+
check=False,
279275
)
276+
if result.returncode != 0:
277+
# Some CI environments cannot keep startup apps alive due to uid/gid
278+
# switching restrictions. Do not skip: require target-specific
279+
# watchdog/recovery evidence from daemon logs instead.
280+
logs = daemon.get_logs()
281+
watchdog_patterns = [
282+
rf"Got kRunning timeout for process.*\(\s*{re.escape(app_name)}\s*\)",
283+
rf"unexpected termination of process.*\(\s*{re.escape(app_name)}\s*\)",
284+
]
285+
assert any(re.search(pattern, logs) for pattern in watchdog_patterns), (
286+
f"Target app {app_name} is not running and no target-specific watchdog diagnostics were found."
287+
f"\nDaemon logs:\n{logs}"
288+
)
289+
return
290+
291+
pid = result.stdout.strip().split("\n")[0]
292+
subprocess.run(["kill", "-STOP", pid], check=True)
293+
try:
294+
# Allow supervision/watchdog loop to detect stalled process.
295+
time.sleep(4.0)
296+
logs = daemon.get_logs()
297+
watchdog_patterns = [
298+
rf"Got kRunning timeout for process.*\(\s*{re.escape(app_name)}\s*\)",
299+
rf"unexpected termination of process.*\(\s*{re.escape(app_name)}\s*\)",
300+
]
301+
assert any(re.search(pattern, logs) for pattern in watchdog_patterns), (
302+
f"No target-specific watchdog diagnostics found for {app_name}.\nDaemon logs:\n{logs}"
303+
)
304+
finally:
305+
subprocess.run(["kill", "-CONT", pid], check=False)

feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/launch_manager_support.cpp

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,25 @@ struct LifecycleTestInput {
6262

6363
const auto& test = test_object_res.value().get();
6464

65-
// Initialize with sensible defaults to prevent zero-initialization issues
66-
LifecycleTestInput input{100, 3};
67-
6865
const auto duration_it = test.find("test_duration_ms");
69-
if (duration_it != test.end()) {
70-
const auto duration_res = duration_it->second.As<uint64_t>();
71-
if (duration_res.has_value()) {
72-
input.test_duration_ms = duration_res.value();
73-
}
66+
if (duration_it == test.end()) {
67+
throw std::invalid_argument("Missing required field: test_duration_ms");
68+
}
69+
const auto duration_res = duration_it->second.As<uint64_t>();
70+
if (!duration_res.has_value()) {
71+
throw std::invalid_argument("Field test_duration_ms must be an unsigned integer");
7472
}
7573

7674
const auto count_it = test.find("checkpoint_count");
77-
if (count_it != test.end()) {
78-
const auto count_res = count_it->second.As<uint64_t>();
79-
// Validate checkpoint_count >= 1 to prevent division by zero
80-
if (count_res.has_value() && count_res.value() >= 1U) {
81-
input.checkpoint_count = static_cast<size_t>(count_res.value());
82-
}
75+
if (count_it == test.end()) {
76+
throw std::invalid_argument("Missing required field: checkpoint_count");
77+
}
78+
const auto count_res = count_it->second.As<uint64_t>();
79+
if (!count_res.has_value()) {
80+
throw std::invalid_argument("Field checkpoint_count must be an unsigned integer");
8381
}
8482

85-
return input;
83+
return LifecycleTestInput{duration_res.value(), static_cast<size_t>(count_res.value())};
8684
}
8785
};
8886

@@ -213,7 +211,7 @@ class ParallelLaunching : public Scenario {
213211
throw std::runtime_error("checkpoint_count must be at least 1");
214212
}
215213

216-
std::cout << "Testing parallel execution pattern with multiple monitors" << std::endl;
214+
std::cout << "Testing parallel health monitoring with multiple monitors" << std::endl;
217215

218216
std::cout << "Started " << std::to_string(test_input.checkpoint_count)
219217
<< " parallel monitors" << std::endl;
@@ -534,22 +532,20 @@ class ConditionalLaunching : public Scenario {
534532
}
535533

536534
std::cout << "Testing conditional launching" << std::endl;
537-
if (!wait_conditions.empty()) {
538-
for (const auto& condition : wait_conditions) {
539-
if (condition.rfind("path:", 0) == 0U) {
540-
std::cout << "Checking path condition: " << condition.substr(5) << std::endl;
541-
} else if (condition.rfind("env:", 0) == 0U) {
542-
std::cout << "Checking env condition: " << condition.substr(4) << std::endl;
543-
} else if (condition.rfind("process:", 0) == 0U) {
544-
std::cout << "Checking process condition: " << condition.substr(8) << std::endl;
545-
} else {
546-
std::cout << "Checking condition: " << condition << std::endl;
547-
}
535+
if (wait_conditions.empty()) {
536+
throw std::runtime_error("Wait conditions were not provided: missing 'test.wait_conditions' in scenario input");
537+
}
538+
539+
for (const auto& condition : wait_conditions) {
540+
if (condition.rfind("path:", 0) == 0U) {
541+
std::cout << "Checking path condition: " << condition.substr(5) << std::endl;
542+
} else if (condition.rfind("env:", 0) == 0U) {
543+
std::cout << "Checking env condition: " << condition.substr(4) << std::endl;
544+
} else if (condition.rfind("process:", 0) == 0U) {
545+
std::cout << "Checking process condition: " << condition.substr(8) << std::endl;
546+
} else {
547+
throw std::runtime_error("Unsupported wait condition prefix: " + condition);
548548
}
549-
} else {
550-
std::cout << "Checking path condition: /tmp/ready" << std::endl;
551-
std::cout << "Checking env condition: STARTUP_COMPLETE" << std::endl;
552-
std::cout << "Checking process condition: init_done" << std::endl;
553549
}
554550
std::cout << "Polling interval: " << polling_interval << "ms" << std::endl;
555551
std::cout << "Condition timeout: " << timeout << "ms" << std::endl;

feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/launch_manager_support.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -340,17 +340,28 @@ impl Scenario for ConditionalLaunching {
340340

341341
info!("Testing conditional launching");
342342

343-
if let Some(conditions) = v["test"]["wait_conditions"].as_array() {
344-
for condition in conditions {
345-
if let Some(cond_str) = condition.as_str() {
346-
if cond_str.starts_with("path:") {
347-
info!("Checking path condition: {}", &cond_str[5..]);
348-
} else if cond_str.starts_with("env:") {
349-
info!("Checking env condition: {}", &cond_str[4..]);
350-
} else if cond_str.starts_with("process:") {
351-
info!("Checking process condition: {}", &cond_str[8..]);
352-
}
353-
}
343+
let conditions = v["test"]["wait_conditions"].as_array().ok_or_else(|| {
344+
"Wait conditions were not provided: missing 'test.wait_conditions' in scenario input".to_string()
345+
})?;
346+
347+
if conditions.is_empty() {
348+
return Err(
349+
"Wait conditions were not provided: empty 'test.wait_conditions' in scenario input".to_string(),
350+
);
351+
}
352+
353+
for condition in conditions {
354+
let cond_str = condition
355+
.as_str()
356+
.ok_or_else(|| "Wait condition entries must be strings".to_string())?;
357+
if cond_str.starts_with("path:") {
358+
info!("Checking path condition: {}", &cond_str[5..]);
359+
} else if cond_str.starts_with("env:") {
360+
info!("Checking env condition: {}", &cond_str[4..]);
361+
} else if cond_str.starts_with("process:") {
362+
info!("Checking process condition: {}", &cond_str[8..]);
363+
} else {
364+
return Err(format!("Unsupported wait condition prefix: {}", cond_str));
354365
}
355366
}
356367

0 commit comments

Comments
 (0)