Skip to content

Commit cfcf198

Browse files
author
Hannes Zietsman
committed
CON-1514 summarize startup failure evidence
1 parent 786e7c8 commit cfcf198

5 files changed

Lines changed: 574 additions & 74 deletions

File tree

tests/cli/test_machines_commands.py

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def test_successful_destroy_does_not_warn_when_instance_is_already_gone(
130130
assert exit_info.value.code == 0
131131
assert destroy_instance.call_count == 1
132132
captured = capsys.readouterr()
133-
assert "Instance 123 destroyed successfully on attempt 1." in captured.out
133+
assert "Temporary test instance 123 destroyed." in captured.out
134134
assert "WARNING: failed to destroy test instance 123" not in captured.out
135135

136136

@@ -193,7 +193,9 @@ def test_ignore_requirements_warns_on_success(self, parse_argv, monkeypatch, cap
193193
assert "Requirement checks are skipped as a pass/fail gate" in out
194194
assert "does not qualify this machine for verification" in out
195195
assert out.count("does not qualify this machine for verification") >= 2
196-
assert "Test passed." in out
196+
assert "Self-test summary" in out
197+
assert "Status: passed" in out
198+
assert "Result: self-test completed successfully." in out
197199

198200
def test_ignore_requirements_warning_in_raw_summary(self, parse_argv, monkeypatch, capsys):
199201
from vastai.cli.commands import machines
@@ -406,7 +408,7 @@ def test_no_offer_non_raw_renders_once_without_stale_placeholders(
406408
search = Mock(side_effect=[[], [broader_offer]])
407409
monkeypatch.setattr("vastai.cli.commands.machines.offers_api.search_offers", search)
408410

409-
args = parse_argv(["self-test", "machine", "42"])
411+
args = parse_argv(["self-test", "machine", "42", "--log-level", "debug"])
410412
with pytest.raises(SystemExit) as exc_info:
411413
args.func(args)
412414

@@ -428,7 +430,7 @@ def test_preflight_outputs_actual_required_once(
428430
search = Mock(return_value=[bad_offer])
429431
monkeypatch.setattr("vastai.cli.commands.machines.offers_api.search_offers", search)
430432

431-
args = parse_argv(["self-test", "machine", "42"])
433+
args = parse_argv(["self-test", "machine", "42", "--log-level", "debug"])
432434
with pytest.raises(SystemExit) as exc_info:
433435
args.func(args)
434436

@@ -442,6 +444,49 @@ def test_preflight_outputs_actual_required_once(
442444
assert "--filter" not in captured.out
443445
assert search.call_count == 1
444446

447+
def test_default_info_preflight_failure_is_compact(
448+
self, parse_argv, patch_get_client, monkeypatch, capsys
449+
):
450+
bad_offer = _self_test_offer(cuda_max_good=11.7, reliability=0.9)
451+
search = Mock(return_value=[bad_offer])
452+
monkeypatch.setattr("vastai.cli.commands.machines.offers_api.search_offers", search)
453+
454+
args = parse_argv(["self-test", "machine", "42"])
455+
with pytest.raises(SystemExit) as exc_info:
456+
args.func(args)
457+
458+
captured = capsys.readouterr()
459+
assert exc_info.value.code == 1
460+
assert "Starting self-test for machine 42." in captured.out
461+
assert "Preflight failed." in captured.out
462+
assert "Self-test summary" in captured.out
463+
assert "Status: failed" in captured.out
464+
assert "Failed checks: CUDA version, Reliability" in captured.out
465+
assert "Reason: 2 preflight requirement check(s) failed." in captured.out
466+
assert "Preflight diagnostics for machine 42 failed:" not in captured.out
467+
assert "actual: 11.7 CUDA" not in captured.out
468+
assert "purpose:" not in captured.out
469+
470+
def test_warning_log_level_keeps_summary_but_suppresses_info_lifecycle(
471+
self, parse_argv, patch_get_client, monkeypatch, capsys
472+
):
473+
bad_offer = _self_test_offer(cuda_max_good=11.7, reliability=0.9)
474+
search = Mock(return_value=[bad_offer])
475+
monkeypatch.setattr("vastai.cli.commands.machines.offers_api.search_offers", search)
476+
477+
args = parse_argv(["self-test", "machine", "42", "--log-level", "warning"])
478+
with pytest.raises(SystemExit) as exc_info:
479+
args.func(args)
480+
481+
captured = capsys.readouterr()
482+
assert exc_info.value.code == 1
483+
assert "Starting self-test for machine 42." not in captured.out
484+
assert "Preflight failed." not in captured.out
485+
assert "Self-test summary" in captured.out
486+
assert "Status: failed" in captured.out
487+
assert "Failed checks: CUDA version, Reliability" in captured.out
488+
assert "actual: 11.7 CUDA" not in captured.out
489+
445490
def test_selected_offer_is_reused_for_rental(
446491
self, parse_argv, patch_get_client, monkeypatch
447492
):
@@ -470,7 +515,7 @@ def test_preflight_normalizes_api_gpu_ram_units(
470515
search = Mock(return_value=[bad_offer])
471516
monkeypatch.setattr("vastai.cli.commands.machines.offers_api.search_offers", search)
472517

473-
args = parse_argv(["self-test", "machine", "42"])
518+
args = parse_argv(["self-test", "machine", "42", "--log-level", "debug"])
474519
with pytest.raises(SystemExit) as exc_info:
475520
args.func(args)
476521

@@ -600,7 +645,7 @@ def test_preflight_direct_port_overage_renders_advisory(
600645
Mock(return_value=[offer]),
601646
)
602647

603-
args = parse_argv(["self-test", "machine", "42"])
648+
args = parse_argv(["self-test", "machine", "42", "--log-level", "debug"])
604649
with pytest.raises(SystemExit) as exc_info:
605650
args.func(args)
606651

@@ -763,6 +808,48 @@ def test_env_test_image_overrides_default_mapping(
763808
assert create.call_args.kwargs["image"] == "vastai/test:p3-env"
764809
assert create.call_args.kwargs["runtype"] == "ssh_direc ssh_proxy"
765810

811+
@pytest.mark.parametrize(
812+
"argv,env_level,expected_level,expected_source",
813+
[
814+
(["self-test", "machine", "42", "--raw"], None, "info", "default"),
815+
(["self-test", "machine", "42", "--log-level", "debug", "--raw"], None, "debug", "argument"),
816+
(["self-test", "machine", "42", "--debugging", "--raw"], None, "debug", "debugging"),
817+
(["self-test", "machine", "42", "--raw"], "debug", "debug", "VAST_LOG_LEVEL"),
818+
(["self-test", "machine", "42", "--raw"], "not-a-level", "info", "default_invalid_VAST_LOG_LEVEL"),
819+
],
820+
)
821+
def test_self_test_log_level_resolution(
822+
self,
823+
parse_argv,
824+
patch_get_client,
825+
monkeypatch,
826+
argv,
827+
env_level,
828+
expected_level,
829+
expected_source,
830+
):
831+
offer = _self_test_offer()
832+
if env_level is None:
833+
monkeypatch.delenv("VAST_LOG_LEVEL", raising=False)
834+
else:
835+
monkeypatch.setenv("VAST_LOG_LEVEL", env_level)
836+
monkeypatch.setattr(
837+
"vastai.cli.commands.machines.offers_api.search_offers",
838+
Mock(return_value=[offer]),
839+
)
840+
monkeypatch.setattr(
841+
"vastai.cli.commands.machines.instances_api.create_instance",
842+
Mock(side_effect=RuntimeError("stop before live rental")),
843+
)
844+
845+
args = parse_argv(argv)
846+
result = args.func(args)
847+
848+
assert result["diagnostics"]["log_level"] == {
849+
"level": expected_level,
850+
"source": expected_source,
851+
}
852+
766853
def test_default_cuda_mapping_still_selects_official_image(
767854
self, parse_argv, patch_get_client, monkeypatch
768855
):
@@ -919,15 +1006,16 @@ def test_cleanup_404_after_destroy_is_not_reported_as_leak(
9191006
monkeypatch.setattr("vastai.cli.commands.machines.instances_api.show_instance", show)
9201007
monkeypatch.setattr("vastai.cli.commands.machines.instances_api.destroy_instance", destroy)
9211008

922-
args = parse_argv(["self-test", "machine", "42"])
1009+
args = parse_argv(["self-test", "machine", "42", "--log-level", "debug"])
9231010
with pytest.raises(SystemExit) as exc_info:
9241011
args.func(args)
9251012

9261013
captured = capsys.readouterr()
9271014
assert exc_info.value.code == 1
928-
assert "Runtime failure diagnostics:" in captured.out
929-
assert "- code: daemon_startup_failed" in captured.out
930-
assert "- remediation: Inspect docker daemon, OCI runtime, and container startup logs." in captured.out
1015+
assert "Runtime failure diagnostics" in captured.out
1016+
assert " code: daemon_startup_failed" in captured.out
1017+
assert " Remediation:" in captured.out
1018+
assert " Inspect docker daemon, OCI runtime, and container startup logs." in captured.out
9311019
assert "WARNING: failed to destroy test instance" not in captured.out
9321020
assert "api_key=secret" not in captured.out
9331021
destroy.assert_called_once()
@@ -947,7 +1035,7 @@ def test_create_instance_error_redacts_api_key(
9471035
)
9481036
monkeypatch.setattr("vastai.cli.commands.machines.instances_api.create_instance", create)
9491037

950-
args = parse_argv(["self-test", "machine", "42"])
1038+
args = parse_argv(["self-test", "machine", "42", "--log-level", "debug"])
9511039
with pytest.raises(SystemExit) as exc_info:
9521040
args.func(args)
9531041

@@ -1080,15 +1168,16 @@ def test_progress_endpoint_failure_prints_external_port(
10801168
Mock(side_effect=requests.exceptions.ConnectTimeout("timed out")),
10811169
)
10821170

1083-
args = parse_argv(["self-test", "machine", "42"])
1171+
args = parse_argv(["self-test", "machine", "42", "--log-level", "debug"])
10841172
with pytest.raises(SystemExit) as exc_info:
10851173
args.func(args)
10861174

10871175
captured = capsys.readouterr()
10881176
assert exc_info.value.code == 1
10891177
assert "External port tested: 45000" in captured.out
1090-
assert "- external port tested: 45000" in captured.out
1091-
assert "- mapped container ports: 22/tcp, 5000/tcp" in captured.out
1178+
assert " Connection attempt:" in captured.out
1179+
assert " external port tested: 45000" in captured.out
1180+
assert " mapped container ports: 22/tcp, 5000/tcp" in captured.out
10921181

10931182
def test_progress_endpoint_lost_after_success_records_different_failure(
10941183
self, parse_argv, patch_get_client, monkeypatch

tests/cli/test_runtime_diagnostics.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,88 @@ def test_status_msg_classifies_generic_daemon_startup_failure():
171171
assert result["underlying_error"] == status_msg
172172

173173

174+
def test_status_msg_classifies_startup_wrapper_build_line():
175+
status_msg = (
176+
"#6 [3/6] RUN export DEBIAN_FRONTEND=noninteractive; "
177+
"apt-get update || echo 'V220614a: error during apt-get update!';"
178+
)
179+
180+
result = diag.classify_status_msg(status_msg)
181+
182+
assert result["code"] == diag.DAEMON_STARTUP_FAILED
183+
assert result["stage"] == diag.STAGE_STARTUP
184+
assert result["underlying_error"] == status_msg
185+
186+
187+
def test_startup_daemon_log_evidence_distinguishes_apt_success_from_cdi_failure():
188+
daemon_log = "\n".join(
189+
[
190+
"#6 [3/6] RUN export DEBIAN_FRONTEND=noninteractive; apt-get update || echo 'V220614a: error during apt-get update!';",
191+
"#6 2.268 Fetched 49.3 MB in 2s (26.6 MB/s)",
192+
"#6 DONE 3.3s",
193+
"Successfully loaded vastai/test:self-test-v2-cuda-11.8",
194+
"Error response from daemon: failed to inject CDI devices: unresolvable CDI devices D.host/gpu=0, D.host/gpu=1, D.host/gpu=2, D.host/gpu=3: unknown",
195+
]
196+
)
197+
198+
evidence = diag.summarize_startup_daemon_log(daemon_log)
199+
200+
assert evidence["apt_update"]["status"] == "completed"
201+
assert evidence["apt_update"]["error_marker_printed"] is False
202+
assert evidence["cdi_gpu_device_injection"]["gpu_ids"] == ["0", "1", "2", "3"]
203+
assert "completed successfully" in evidence["findings"][0]
204+
205+
206+
def test_startup_failure_refinement_prints_host_runtime_cause_for_cdi():
207+
diagnostic = diag.classify_status_msg(
208+
"#6 [3/6] RUN export DEBIAN_FRONTEND=noninteractive; apt-get update || echo 'V220614a: error during apt-get update!';"
209+
)
210+
daemon_log = "\n".join(
211+
[
212+
"#6 [3/6] RUN export DEBIAN_FRONTEND=noninteractive; apt-get update || echo 'V220614a: error during apt-get update!';",
213+
"#6 DONE 3.3s",
214+
"Error response from daemon: failed to inject CDI devices: unresolvable CDI devices D.host/gpu=0: unknown",
215+
]
216+
)
217+
218+
refined = diag.refine_startup_failure_with_daemon_log(diagnostic, daemon_log)
219+
220+
assert refined["code"] == diag.DAEMON_STARTUP_FAILED
221+
assert refined["summary"] == "Docker/NVIDIA runtime failed before the self-test container could start."
222+
assert "NVIDIA container runtime/CDI" in refined["remediation"]
223+
assert refined["startup_evidence"]["apt_update"]["status"] == "completed"
224+
assert refined["startup_evidence"]["cdi_gpu_device_injection"]["gpu_ids"] == ["0"]
225+
226+
227+
def test_startup_failure_refinement_uses_status_message_when_daemon_log_lacks_cdi():
228+
diagnostic = diag.classify_status_msg(
229+
"Error response from daemon: failed to create task for container: failed to inject CDI devices: "
230+
"unresolvable CDI devices D.host/gpu=0, D.host/gpu=1: unknown"
231+
)
232+
daemon_log = "container setup log did not include the final status error"
233+
234+
refined = diag.refine_startup_failure_with_daemon_log(diagnostic, daemon_log)
235+
236+
assert refined["summary"] == "Docker/NVIDIA runtime failed before the self-test container could start."
237+
assert refined["startup_evidence"]["cdi_gpu_device_injection"]["gpu_ids"] == ["0", "1"]
238+
assert refined["startup_evidence"]["sources"] == ["instance/daemon.log", "instance status message"]
239+
240+
241+
def test_startup_daemon_log_evidence_detects_actual_apt_update_marker_output():
242+
daemon_log = "\n".join(
243+
[
244+
"#6 [3/6] RUN export DEBIAN_FRONTEND=noninteractive; apt-get update || echo 'V220614a: error during apt-get update!';",
245+
"#6 2.100 V220614a: error during apt-get update!",
246+
"#6 DONE 2.2s",
247+
]
248+
)
249+
250+
evidence = diag.summarize_startup_daemon_log(daemon_log)
251+
252+
assert evidence["apt_update"]["status"] == "failed"
253+
assert evidence["apt_update"]["error_marker_printed"] is True
254+
255+
174256
def test_status_msg_classifies_other_errors_as_status_error():
175257
result = diag.classify_status_msg("Error: host reported an unknown fault")
176258

tests/cli/test_self_test_support_bundle.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ def fake_bundle(**kwargs):
105105

106106
captured = capsys.readouterr()
107107
assert exc_info.value.code == 1
108-
assert "Self-test diagnostic bundle saved to:" in captured.out
109-
assert "self-test-result.json" in captured.out
110-
assert "Review this tarball before sharing it with support." in captured.out
108+
assert f"Support bundle: {tmp_path / 'vast_selftest_42_20260602T100000Z.tar.gz'}" in captured.out
109+
assert "self-test-result.json" not in captured.out
110+
assert "Next: inspect the support bundle or rerun with --log-level debug" in captured.out
111111

112112

113113
def test_self_test_bundle_creation_error_preserves_original_failure(
@@ -144,11 +144,11 @@ def test_self_test_bundle_creation_error_preserves_original_failure(
144144
captured = capsys.readouterr()
145145
assert exc_info.value.code == 1
146146
assert "WARNING: failed to create self-test diagnostic bundle: disk full" in captured.out
147-
assert "Test failed: 8 preflight requirement check(s) failed." in captured.out
147+
assert "Reason: 8 preflight requirement check(s) failed." in captured.out
148148

149149

150150
def test_self_test_runtime_failure_bundle_includes_instance_logs(
151-
parse_argv, patch_get_client, monkeypatch, tmp_path
151+
parse_argv, patch_get_client, monkeypatch, tmp_path, capsys
152152
):
153153
from vastai.cli.commands import machines
154154

@@ -198,7 +198,10 @@ def fake_show_instance(client, id):
198198
"id": id,
199199
"actual_status": "created",
200200
"intended_status": "running",
201-
"status_msg": "docker_build() error writing dockerfile",
201+
"status_msg": (
202+
"#6 [3/6] RUN export DEBIAN_FRONTEND=noninteractive; "
203+
"apt-get update || echo 'V220614a: error during apt-get update!';"
204+
),
202205
"label": "vast-self-test-machine-42",
203206
}
204207

@@ -207,7 +210,16 @@ def fake_logs(client, instance_id, tail=None, filter=None, daemon_logs=False):
207210
assert instance_id == 123
208211
assert tail == machines.INSTANCE_LOG_TAIL_LINES
209212
assert filter is None
210-
return "daemon startup failure" if daemon_logs else "container startup failure"
213+
if daemon_logs:
214+
return "\n".join(
215+
[
216+
"#6 [3/6] RUN export DEBIAN_FRONTEND=noninteractive; apt-get update || echo 'V220614a: error during apt-get update!';",
217+
"#6 2.268 Fetched 49.3 MB in 2s (26.6 MB/s)",
218+
"#6 DONE 3.3s",
219+
"Error response from daemon: failed to inject CDI devices: unresolvable CDI devices D.host/gpu=0, D.host/gpu=1: unknown",
220+
]
221+
)
222+
return "container startup failure"
211223

212224
def fake_destroy_instance(client, id):
213225
assert client is patch_get_client
@@ -222,18 +234,37 @@ def fake_destroy_instance(client, id):
222234
monkeypatch.setattr(machines.instances_api, "logs", fake_logs)
223235
monkeypatch.setattr(machines.instances_api, "destroy_instance", fake_destroy_instance)
224236

225-
args = parse_argv(["self-test", "machine", "42", "--support-bundle-dir", str(tmp_path)])
237+
args = parse_argv([
238+
"self-test",
239+
"machine",
240+
"42",
241+
"--support-bundle-dir",
242+
str(tmp_path),
243+
"--log-level",
244+
"debug",
245+
])
226246
with pytest.raises(SystemExit) as exc_info:
227247
args.func(args)
228248

229249
assert exc_info.value.code == 1
250+
captured = capsys.readouterr()
230251
assert 123 in destroyed
231252
assert created["include_local_host_artifacts"] is False
232253
assert created["result"]["failure_code"] == "daemon_startup_failed"
254+
assert created["result"]["failure"]["summary"] == "Docker/NVIDIA runtime failed before the self-test container could start."
255+
assert created["result"]["failure"]["startup_evidence"]["apt_update"]["status"] == "completed"
233256
assert created["extra_files"]["instance/container.log"] == "container startup failure"
234-
assert created["extra_files"]["instance/daemon.log"] == "daemon startup failure"
257+
assert "failed to inject CDI devices" in created["extra_files"]["instance/daemon.log"]
235258
assert '"label": "vast-self-test-machine-42"' in created["extra_files"]["instance/show-instance.json"]
236259
assert created["extra_errors"] == []
260+
assert "Runtime failure diagnostics" in captured.out
261+
assert " Result:" in captured.out
262+
assert " What happened:" in captured.out
263+
assert "Vast startup wrapper apt-get update completed successfully." in captured.out
264+
assert "Docker/NVIDIA runtime failed to inject CDI GPU devices for GPUs 0, 1." in captured.out
265+
assert " Where to read next:" in captured.out
266+
assert "instance/daemon.log in the diagnostic bundle" in captured.out
267+
assert "Reason: Docker/NVIDIA runtime failed before the self-test container could start." in captured.out
237268

238269

239270
def test_dump_logs_command_creates_cli_visible_bundle(parse_argv, monkeypatch, tmp_path):

0 commit comments

Comments
 (0)