Skip to content

Commit 434cbf0

Browse files
committed
Improve CLI compatibility and harden multipass tests
1 parent dd393f0 commit 434cbf0

17 files changed

+403
-239
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<img src="docs/img/lb_mark.svg" width="120" alt="Linux Benchmark Library logo" />
2+
<img src="docs/img/lb_mark.png" width="120" alt="Linux Benchmark Library logo" />
33
</p>
44

55
<h1 align="center">Linux Benchmark Library</h1>
@@ -15,6 +15,19 @@
1515
<a href="https://github.com/miciav/linux-benchmark-lib/releases">Releases</a>
1616
</p>
1717

18+
<p align="center">
19+
<a href="https://github.com/miciav/linux-benchmark-lib/actions/workflows/pages.yml">
20+
<img src="https://github.com/miciav/linux-benchmark-lib/actions/workflows/pages.yml/badge.svg" alt="Docs build" />
21+
</a>
22+
<a href="https://github.com/miciav/linux-benchmark-lib/actions/workflows/diagrams.yml">
23+
<img src="https://github.com/miciav/linux-benchmark-lib/actions/workflows/diagrams.yml/badge.svg" alt="Diagrams build" />
24+
</a>
25+
<a href="https://github.com/miciav/linux-benchmark-lib/blob/main/LICENSE">
26+
<img src="https://img.shields.io/github/license/miciav/linux-benchmark-lib" alt="License" />
27+
</a>
28+
<img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python versions" />
29+
</p>
30+
1831
## Highlights
1932

2033
- Layered architecture: runner, controller, app, UI, analytics.

implementation_summary.md

Lines changed: 0 additions & 34 deletions
This file was deleted.

lb_ui/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import os
10+
import subprocess
1011
from typing import Optional
1112

1213
import typer

lb_ui/commands/plugin.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,24 @@ def plugin_list(
127127
config=config, enable=enable, disable=disable, set_default=set_default, select=select
128128
)
129129

130+
@app.command("select")
131+
def plugin_select(
132+
config: Optional[Path] = typer.Option(
133+
None, "--config", "-c", help="Config file to update when enabling/disabling."
134+
),
135+
set_default: bool = typer.Option(
136+
False,
137+
"--set-default/--no-set-default",
138+
help="Remember the config after enabling/disabling.",
139+
),
140+
) -> None:
141+
"""Interactive plugin selection (compatibility command)."""
142+
_list_plugins_command(
143+
config=config,
144+
enable=None,
145+
disable=None,
146+
set_default=set_default,
147+
select=True,
148+
)
149+
130150
return app
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Helpers to exercise controller stop lifecycle (no VM, delegated driver)."""
2+
3+
from __future__ import annotations
4+
5+
import threading
6+
import time
7+
from pathlib import Path
8+
9+
from lb_controller.ansible_executor import AnsibleRunnerExecutor
10+
from lb_controller.api import BenchmarkController
11+
from lb_runner.benchmark_config import (
12+
BenchmarkConfig,
13+
RemoteExecutionConfig,
14+
RemoteHostConfig,
15+
WorkloadConfig,
16+
)
17+
from lb_runner.stop_token import StopToken
18+
19+
SCENARIO_ROOT = Path(__file__).resolve().parents[1]
20+
PLAYBOOK_ROOT = SCENARIO_ROOT / "playbooks"
21+
22+
23+
def _config(output_root: Path) -> BenchmarkConfig:
24+
cfg = BenchmarkConfig(
25+
output_dir=output_root,
26+
report_dir=output_root / "reports",
27+
data_export_dir=output_root / "exports",
28+
remote_hosts=[
29+
RemoteHostConfig(
30+
name="simulated",
31+
address="127.0.0.1",
32+
user="runner",
33+
become=False,
34+
vars={
35+
"ansible_connection": "local",
36+
"ansible_python_interpreter": "python3",
37+
},
38+
)
39+
],
40+
)
41+
cfg.workloads = {"dummy": WorkloadConfig(plugin="stress_ng", enabled=True)}
42+
cfg.remote_execution = RemoteExecutionConfig(
43+
enabled=True,
44+
setup_playbook=PLAYBOOK_ROOT / "setup.yml",
45+
run_playbook=PLAYBOOK_ROOT / "run.yml",
46+
collect_playbook=PLAYBOOK_ROOT / "run.yml",
47+
teardown_playbook=PLAYBOOK_ROOT / "teardown.yml",
48+
run_collect=False,
49+
run_setup=True,
50+
run_teardown=True,
51+
)
52+
cfg.repetitions = 1
53+
return cfg
54+
55+
56+
def run_controller(stop_at: str | None) -> dict[str, bool]:
57+
out = Path.cwd() / "controller_artifacts" / (stop_at or "clean")
58+
out.mkdir(parents=True, exist_ok=True)
59+
run_id = f"molecule-{stop_at or 'clean'}"
60+
lb_workdir = out / "lb_workdir"
61+
lb_workdir.mkdir(parents=True, exist_ok=True)
62+
cfg = _config(out)
63+
stop_token = StopToken(enable_signals=False)
64+
executor = AnsibleRunnerExecutor(stream_output=True, stop_token=stop_token)
65+
controller = BenchmarkController(cfg, executor=executor, stop_token=stop_token)
66+
67+
class _StubPlugin:
68+
name = "stub"
69+
70+
def get_ansible_setup_path(self):
71+
return None
72+
73+
def get_ansible_teardown_path(self):
74+
return None
75+
76+
def get_ansible_setup_extravars(self):
77+
return {}
78+
79+
def get_ansible_teardown_extravars(self):
80+
return {}
81+
82+
class _StubRegistry:
83+
def get(self, name: str):
84+
return _StubPlugin()
85+
86+
controller.plugin_registry = _StubRegistry()
87+
88+
if stop_at:
89+
def arm_stop() -> None:
90+
delay = 0.1 if stop_at == "setup" else 1.5 if stop_at == "run" else 4.0
91+
time.sleep(delay)
92+
stop_token.request_stop()
93+
94+
threading.Thread(target=arm_stop, daemon=True).start()
95+
96+
def _patched_handle(inventory, extravars, log_fn):
97+
patched = extravars.copy()
98+
patched["lb_workdir"] = str(lb_workdir)
99+
try:
100+
(lb_workdir / "STOP").touch()
101+
except Exception:
102+
pass
103+
return True
104+
105+
controller._handle_stop_protocol = _patched_handle # type: ignore[attr-defined]
106+
107+
summary = controller.run(test_types=["dummy"], run_id=run_id)
108+
run_path = out / run_id
109+
markers = {
110+
"setup": (run_path / "setup_marker").exists(),
111+
"run_start": (run_path / "run_start").exists(),
112+
"run_done": (run_path / "run_done").exists(),
113+
"teardown": (run_path / "teardown_marker").exists(),
114+
"success": summary.success,
115+
}
116+
return markers
117+
118+
119+
def run_all_cases() -> dict[str, dict[str, bool]]:
120+
return {
121+
"clean": run_controller(None),
122+
"setup_interrupt": run_controller("setup"),
123+
"run_interrupt": run_controller("run"),
124+
"teardown_interrupt": run_controller("teardown"),
125+
}
126+
127+
128+
def main() -> None:
129+
cases = run_all_cases()
130+
for name, markers in cases.items():
131+
print(f"[{name}] {markers}")
132+
if name == "clean":
133+
assert markers["success"] is True
134+
assert markers["setup"] and markers["run_done"] and markers["teardown"]
135+
elif name == "setup_interrupt":
136+
assert markers["teardown"], "Teardown must run after setup interruption"
137+
elif name == "run_interrupt":
138+
assert markers["setup"]
139+
assert markers["teardown"], "Teardown must run after run interruption"
140+
elif name == "teardown_interrupt":
141+
assert markers["setup"]
142+
assert markers["teardown"]
143+
144+
145+
if __name__ == "__main__":
146+
main()

molecule/controller-stop/scripts/test_controller_stop.py

Lines changed: 2 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,10 @@
11
"""Molecule verification script for controller stop lifecycle (no VM, delegated driver)."""
22

3-
from __future__ import annotations
4-
5-
import threading
6-
import time
7-
from pathlib import Path
8-
9-
from lb_controller.ansible_executor import AnsibleRunnerExecutor
10-
from lb_controller.api import BenchmarkController
11-
from lb_runner.benchmark_config import (
12-
BenchmarkConfig,
13-
RemoteExecutionConfig,
14-
RemoteHostConfig,
15-
WorkloadConfig,
16-
)
17-
from lb_runner.stop_token import StopToken
18-
19-
20-
SCENARIO_ROOT = Path(__file__).resolve().parents[1]
21-
PLAYBOOK_ROOT = SCENARIO_ROOT / "playbooks"
22-
23-
24-
def _config(output_root: Path) -> BenchmarkConfig:
25-
cfg = BenchmarkConfig(
26-
output_dir=output_root,
27-
report_dir=output_root / "reports",
28-
data_export_dir=output_root / "exports",
29-
remote_hosts=[
30-
RemoteHostConfig(
31-
name="simulated",
32-
address="127.0.0.1",
33-
user="runner",
34-
become=False,
35-
vars={
36-
"ansible_connection": "local",
37-
"ansible_python_interpreter": "python3",
38-
},
39-
)
40-
],
41-
)
42-
cfg.workloads = {"dummy": WorkloadConfig(plugin="stress_ng", enabled=True)}
43-
cfg.remote_execution = RemoteExecutionConfig(
44-
enabled=True,
45-
setup_playbook=PLAYBOOK_ROOT / "setup.yml",
46-
run_playbook=PLAYBOOK_ROOT / "run.yml",
47-
collect_playbook=PLAYBOOK_ROOT / "run.yml",
48-
teardown_playbook=PLAYBOOK_ROOT / "teardown.yml",
49-
run_collect=False,
50-
run_setup=True,
51-
run_teardown=True,
52-
)
53-
cfg.repetitions = 1
54-
return cfg
55-
56-
57-
def run_controller(stop_at: str | None) -> dict[str, bool]:
58-
out = Path.cwd() / "controller_artifacts" / (stop_at or "clean")
59-
out.mkdir(parents=True, exist_ok=True)
60-
run_id = f"molecule-{stop_at or 'clean'}"
61-
lb_workdir = out / "lb_workdir"
62-
lb_workdir.mkdir(parents=True, exist_ok=True)
63-
cfg = _config(out)
64-
stop_token = StopToken(enable_signals=False)
65-
executor = AnsibleRunnerExecutor(stream_output=True, stop_token=stop_token)
66-
controller = BenchmarkController(cfg, executor=executor, stop_token=stop_token)
67-
# Inject a stub plugin registry to avoid invoking real workload setup/teardown.
68-
class _StubPlugin:
69-
name = "stub"
70-
71-
def get_ansible_setup_path(self):
72-
return None
73-
74-
def get_ansible_teardown_path(self):
75-
return None
76-
77-
def get_ansible_setup_extravars(self):
78-
return {}
79-
80-
def get_ansible_teardown_extravars(self):
81-
return {}
82-
83-
class _StubRegistry:
84-
def get(self, name: str):
85-
return _StubPlugin()
86-
87-
controller.plugin_registry = _StubRegistry()
88-
89-
if stop_at:
90-
def arm_stop() -> None:
91-
# Let phase progress a bit
92-
delay = 0.1 if stop_at == "setup" else 1.5 if stop_at == "run" else 4.0
93-
time.sleep(delay)
94-
stop_token.request_stop()
95-
96-
threading.Thread(target=arm_stop, daemon=True).start()
97-
98-
def _patched_handle(inventory, extravars, log_fn):
99-
# Bypass distributed stop wait; simulate immediate stop confirmation.
100-
patched = extravars.copy()
101-
patched["lb_workdir"] = str(lb_workdir)
102-
try:
103-
(lb_workdir / "STOP").touch()
104-
except Exception:
105-
pass
106-
return True
107-
108-
controller._handle_stop_protocol = _patched_handle # type: ignore[attr-defined]
109-
110-
summary = controller.run(test_types=["dummy"], run_id=run_id)
111-
run_path = out / run_id
112-
markers = {
113-
"setup": (run_path / "setup_marker").exists(),
114-
"run_start": (run_path / "run_start").exists(),
115-
"run_done": (run_path / "run_done").exists(),
116-
"teardown": (run_path / "teardown_marker").exists(),
117-
"success": summary.success,
118-
}
119-
return markers
3+
from controller_stop_runner import run_all_cases # type: ignore
1204

1215

1226
def main() -> None:
123-
cases = {
124-
"clean": run_controller(None),
125-
"setup_interrupt": run_controller("setup"),
126-
"run_interrupt": run_controller("run"),
127-
"teardown_interrupt": run_controller("teardown"),
128-
}
7+
cases = run_all_cases()
1298
for name, markers in cases.items():
1309
print(f"[{name}] {markers}")
13110
if name == "clean":

0 commit comments

Comments
 (0)