diff --git a/clients/deadline.py b/clients/deadline.py index c0cfc94..eeb9770 100644 --- a/clients/deadline.py +++ b/clients/deadline.py @@ -29,6 +29,10 @@ def elapsed(self) -> float: """Return seconds since start; uncapped, may exceed timeout.""" return self._clock.monotonic() - self._started_at + def elapsed_ms(self) -> float: + """Return milliseconds since start; uncapped, may exceed timeout.""" + return self.elapsed() * 1000 + def fraction_used(self) -> float: """Return elapsed/timeout clamped to [0, 1]; timeout==0 yields 1.0.""" if self._timeout == 0: diff --git a/scenarios/cli.py b/scenarios/cli.py index 2e51c8f..9d2b999 100644 --- a/scenarios/cli.py +++ b/scenarios/cli.py @@ -84,6 +84,12 @@ def build_parser() -> argparse.ArgumentParser: "find", help="Find scenarios by id or description substring" ) find_parser.add_argument("query", help="Case-insensitive match on id or description") + find_parser.add_argument( + "--format", + choices=("text", "json"), + default="text", + help="Output format (default: text)", + ) return parser @@ -210,11 +216,12 @@ def main(argv: list[str] | None = None) -> int: if query in entry.scenario_id.casefold() or query in entry.description.casefold() ] - if not matches: - return 1 - for scenario_id in matches: - print(scenario_id) - return 0 + if args.format == "json": + print(json.dumps(matches, indent=2)) + else: + for scenario_id in matches: + print(scenario_id) + return 0 if matches else 1 return 1 diff --git a/tests/test_cli.py b/tests/test_cli.py index 19556a5..979cc65 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -275,6 +275,30 @@ def test_find_no_matches_returns_error(self): self.assertEqual(code, 1) self.assertEqual(buffer.getvalue().strip(), "") + def test_find_json_format(self): + buffer = io.StringIO() + with redirect_stdout(buffer): + code = main(["find", "CIRCUIT-BREAKER", "--format", "json"]) + self.assertEqual(code, 0) + parsed = json.loads(buffer.getvalue()) + self.assertEqual(parsed, ["circuit-breaker"]) + + def test_find_text_format_default_unchanged(self): + buffer = io.StringIO() + with redirect_stdout(buffer): + code = main(["find", "CIRCUIT-BREAKER", "--format", "text"]) + self.assertEqual(code, 0) + lines = [line for line in buffer.getvalue().splitlines() if line.strip()] + self.assertEqual(lines, ["circuit-breaker"]) + + def test_find_json_no_matches_returns_empty_array(self): + buffer = io.StringIO() + with redirect_stdout(buffer): + code = main(["find", "zzzz-no-such", "--format", "json"]) + self.assertEqual(code, 1) + parsed = json.loads(buffer.getvalue()) + self.assertEqual(parsed, []) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_deadline.py b/tests/test_deadline.py index 7890cdf..a3b6627 100644 --- a/tests/test_deadline.py +++ b/tests/test_deadline.py @@ -51,6 +51,16 @@ def test_elapsed_tracks_fake_clock_uncapped(self): self.assertAlmostEqual(deadline.elapsed(), 0.8) self.assertGreater(deadline.elapsed(), 0.5) + def test_elapsed_ms_tracks_fake_clock_uncapped(self): + clock = FakeClock() + deadline = Deadline(clock, timeout=0.5) + self.assertAlmostEqual(deadline.elapsed_ms(), 0.0) + clock.sleep(0.4) + self.assertAlmostEqual(deadline.elapsed_ms(), 400.0) + clock.sleep(0.4) + self.assertAlmostEqual(deadline.elapsed_ms(), 800.0) + self.assertGreater(deadline.elapsed_ms(), 500.0) + def test_fraction_used_clamped_to_unit_interval(self): clock = FakeClock() deadline = Deadline(clock, timeout=1.0)