Skip to content

Commit f0ddf54

Browse files
committed
fix: address CR feedback on CLI entry point
- Use 'if val is not None' instead of 'if val' for boolean flags to handle future default=True cases correctly - Refactor cmd_run to delegate to cmd_execute and cmd_collect instead of reimplementing pipeline logic inline - Add --attach flag to civ run for reusing existing infrastructure
1 parent ef4ffa7 commit f0ddf54

2 files changed

Lines changed: 59 additions & 93 deletions

File tree

‎cli.py‎

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from core.config import CoreConfig
1111
from core.executor import execute
12-
from core.metadata import InstanceMetadata
12+
from core.metadata import InstanceMetadata, write_instances_json
1313
from core.provisioner import Provisioner
1414
from core.results import get_exit_code, merge_results
1515

@@ -39,7 +39,7 @@ def _build_config(args: argparse.Namespace) -> CoreConfig:
3939

4040
for attr in ("debug", "parallel", "stop_cleanup"):
4141
val = getattr(args, attr, None)
42-
if val:
42+
if val is not None:
4343
config_dict[attr] = val
4444

4545
if getattr(args, "tags", None):
@@ -124,37 +124,32 @@ def cmd_run(args: argparse.Namespace) -> int:
124124
config = _build_config(args)
125125
provisioner = Provisioner(config)
126126
try:
127-
instances = provisioner.provision()
128-
provisioner.prepare_environment(instances)
127+
if getattr(args, "attach", False):
128+
instances = provisioner.get_instances()
129+
write_instances_json(instances, config.instances_json)
130+
else:
131+
instances = provisioner.provision()
132+
provisioner.prepare_environment(instances)
129133

130134
command = getattr(args, "command", None) or _build_test_command(config)
131135
results_dir = os.path.dirname(config.output_file) or "/tmp"
132-
exec_result = execute(
133-
instances=instances,
136+
137+
exec_args = argparse.Namespace(
138+
instances_json=config.instances_json,
134139
command=command,
135140
ssh_config=config.ssh_config_file,
136141
results_dir=results_dir,
137142
timeout=getattr(args, "timeout", 3600),
138143
)
144+
exit_code = cmd_execute(exec_args)
139145

140-
for ir in exec_result.instance_results:
141-
status = "PASS" if ir.exit_code == 0 else "FAIL"
142-
print(f" [{status}] {ir.instance_name} (exit={ir.exit_code})")
143-
144-
result_files = [
145-
r.result_file for r in exec_result.instance_results if r.result_file
146-
]
147-
if not result_files:
148-
print("No result files collected")
149-
return 1
150-
151-
merge_result = merge_results(result_files, config.output_file)
152-
print(
153-
f"Merged {merge_result.total_tests} tests: "
154-
f"{merge_result.failures} failures, "
155-
f"{merge_result.errors} errors"
146+
collect_args = argparse.Namespace(
147+
results_dir=results_dir,
148+
output_file=config.output_file,
156149
)
157-
return get_exit_code(merge_result)
150+
collect_code = cmd_collect(collect_args)
151+
152+
return collect_code if collect_code != 0 else exit_code
158153

159154
except Exception as exc:
160155
print(f"Error: {exc}")
@@ -250,6 +245,8 @@ def build_parser() -> argparse.ArgumentParser:
250245
help="Custom command (overrides test flags)")
251246
p_run.add_argument("--timeout", type=int, default=3600,
252247
help="Timeout in seconds (default: 3600)")
248+
p_run.add_argument("-a", "--attach", action="store_true", default=False,
249+
help="Attach to existing infrastructure instead of provisioning")
253250
p_run.set_defaults(func=cmd_run)
254251

255252
# --- cleanup ---

‎test/test_cli.py‎

Lines changed: 39 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def _run_args(**overrides):
5555
"tags": None,
5656
"command": None,
5757
"timeout": 3600,
58+
"attach": False,
5859
}
5960
defaults.update(overrides)
6061
return Namespace(**defaults)
@@ -174,7 +175,7 @@ def test_run_subcommand_all_flags(self):
174175
"-t", "test_ssh", "--test-suites", "a/", "b/",
175176
"-m", "pub", "-p", "-d", "-s", "-e", "automated",
176177
"--tags", "env:prod", "--command", "echo hi",
177-
"--timeout", "600",
178+
"--timeout", "600", "-a",
178179
])
179180
assert args.resources_file == "resources.json"
180181
assert args.test_filter == "test_ssh"
@@ -187,6 +188,7 @@ def test_run_subcommand_all_flags(self):
187188
assert args.tags == "env:prod"
188189
assert args.command == "echo hi"
189190
assert args.timeout == 600
191+
assert args.attach is True
190192

191193
def test_cleanup_subcommand(self):
192194
parser = build_parser()
@@ -310,70 +312,42 @@ def test_returns_two_when_no_files(self, mock_glob):
310312

311313

312314
class TestCmdRun:
313-
@patch("cli.merge_results")
314-
@patch("cli.execute")
315+
@patch("cli.cmd_collect", return_value=0)
316+
@patch("cli.cmd_execute", return_value=0)
315317
@patch("cli.Provisioner")
316-
def test_full_pipeline_success(self, MockProv, mock_execute, mock_merge):
318+
def test_full_pipeline_success(self, MockProv, mock_exec, mock_coll):
317319
mock_prov = MockProv.return_value
318320
mock_prov.provision.return_value = {"inst1": MagicMock()}
319321

320-
mock_ir = MagicMock()
321-
mock_ir.exit_code = 0
322-
mock_ir.instance_name = "inst1"
323-
mock_ir.result_file = "/tmp/r/inst1.xml"
324-
mock_exec_result = MagicMock()
325-
mock_exec_result.instance_results = [mock_ir]
326-
mock_execute.return_value = mock_exec_result
327-
328-
mock_merge.return_value = MergeResult(
329-
total_tests=5, failures=0, errors=0, skipped=0, time=1.0,
330-
)
331-
332322
args = _run_args()
333323
assert cmd_run(args) == 0
334324
mock_prov.provision.assert_called_once()
335325
mock_prov.prepare_environment.assert_called_once()
326+
mock_exec.assert_called_once()
327+
mock_coll.assert_called_once()
336328
mock_prov.cleanup.assert_called_once()
337329

338-
@patch("cli.merge_results")
339-
@patch("cli.execute")
330+
@patch("cli.cmd_collect", return_value=1)
331+
@patch("cli.cmd_execute", return_value=0)
340332
@patch("cli.Provisioner")
341-
def test_returns_exit_code_from_merge(self, MockProv, mock_execute, mock_merge):
333+
def test_returns_collect_exit_code(self, MockProv, mock_exec, mock_coll):
342334
mock_prov = MockProv.return_value
343335
mock_prov.provision.return_value = {"inst1": MagicMock()}
344336

345-
mock_ir = MagicMock()
346-
mock_ir.exit_code = 0
347-
mock_ir.instance_name = "inst1"
348-
mock_ir.result_file = "/tmp/r/inst1.xml"
349-
mock_exec_result = MagicMock()
350-
mock_exec_result.instance_results = [mock_ir]
351-
mock_execute.return_value = mock_exec_result
352-
353-
mock_merge.return_value = MergeResult(
354-
total_tests=5, failures=2, errors=0, skipped=0, time=1.0,
355-
)
356-
357337
args = _run_args()
358338
assert cmd_run(args) == 1
359339

360-
@patch("cli.execute")
340+
@patch("cli.cmd_collect", return_value=0)
341+
@patch("cli.cmd_execute", return_value=1)
361342
@patch("cli.Provisioner")
362-
def test_returns_one_when_no_result_files(self, MockProv, mock_execute):
343+
def test_returns_execute_exit_code_when_collect_passes(
344+
self, MockProv, mock_exec, mock_coll,
345+
):
363346
mock_prov = MockProv.return_value
364347
mock_prov.provision.return_value = {"inst1": MagicMock()}
365348

366-
mock_ir = MagicMock()
367-
mock_ir.exit_code = 1
368-
mock_ir.instance_name = "inst1"
369-
mock_ir.result_file = None
370-
mock_exec_result = MagicMock()
371-
mock_exec_result.instance_results = [mock_ir]
372-
mock_execute.return_value = mock_exec_result
373-
374349
args = _run_args()
375350
assert cmd_run(args) == 1
376-
mock_prov.cleanup.assert_called_once()
377351

378352
@patch("cli.Provisioner")
379353
def test_returns_100_on_infra_error(self, MockProv):
@@ -384,48 +358,43 @@ def test_returns_100_on_infra_error(self, MockProv):
384358
assert cmd_run(args) == 100
385359
mock_prov.cleanup.assert_called_once()
386360

387-
@patch("cli.execute")
361+
@patch("cli.cmd_collect", return_value=0)
362+
@patch("cli.cmd_execute", return_value=0)
388363
@patch("cli.Provisioner")
389-
def test_skips_cleanup_when_stop_cleanup(self, MockProv, mock_execute):
364+
def test_skips_cleanup_when_stop_cleanup(self, MockProv, mock_exec, mock_coll):
390365
mock_prov = MockProv.return_value
391366
mock_prov.provision.return_value = {"inst1": MagicMock()}
392367

393-
mock_ir = MagicMock()
394-
mock_ir.result_file = None
395-
mock_ir.exit_code = 0
396-
mock_ir.instance_name = "inst1"
397-
mock_exec_result = MagicMock()
398-
mock_exec_result.instance_results = [mock_ir]
399-
mock_execute.return_value = mock_exec_result
400-
401368
args = _run_args(stop_cleanup=True)
402369
cmd_run(args)
403370
mock_prov.cleanup.assert_not_called()
404371

405-
@patch("cli.merge_results")
406-
@patch("cli.execute")
372+
@patch("cli.cmd_collect", return_value=0)
373+
@patch("cli.cmd_execute", return_value=0)
407374
@patch("cli.Provisioner")
408-
def test_uses_custom_command(self, MockProv, mock_execute, mock_merge):
375+
def test_passes_custom_command(self, MockProv, mock_exec, mock_coll):
409376
mock_prov = MockProv.return_value
410377
mock_prov.provision.return_value = {"inst1": MagicMock()}
411378

412-
mock_ir = MagicMock()
413-
mock_ir.exit_code = 0
414-
mock_ir.instance_name = "inst1"
415-
mock_ir.result_file = "/tmp/r/inst1.xml"
416-
mock_exec_result = MagicMock()
417-
mock_exec_result.instance_results = [mock_ir]
418-
mock_execute.return_value = mock_exec_result
419-
420-
mock_merge.return_value = MergeResult(
421-
total_tests=1, failures=0, errors=0, skipped=0, time=0.5,
422-
)
423-
424379
args = _run_args(command="echo hi")
425380
cmd_run(args)
426-
mock_execute.assert_called_once()
427-
call_kwargs = mock_execute.call_args
428-
assert call_kwargs[1]["command"] == "echo hi"
381+
exec_call_args = mock_exec.call_args[0][0]
382+
assert exec_call_args.command == "echo hi"
383+
384+
@patch("cli.cmd_collect", return_value=0)
385+
@patch("cli.cmd_execute", return_value=0)
386+
@patch("cli.write_instances_json")
387+
@patch("cli.Provisioner")
388+
def test_attach_skips_provision(self, MockProv, mock_write, mock_exec, mock_coll):
389+
mock_prov = MockProv.return_value
390+
mock_prov.get_instances.return_value = {"inst1": MagicMock()}
391+
392+
args = _run_args(attach=True)
393+
assert cmd_run(args) == 0
394+
mock_prov.provision.assert_not_called()
395+
mock_prov.prepare_environment.assert_not_called()
396+
mock_prov.get_instances.assert_called_once()
397+
mock_write.assert_called_once()
429398

430399

431400
class TestCmdCleanup:

0 commit comments

Comments
 (0)