diff --git a/clients/deadline.py b/clients/deadline.py index f9abe47..c0cfc94 100644 --- a/clients/deadline.py +++ b/clients/deadline.py @@ -50,6 +50,10 @@ def remaining(self) -> float: left = self._timeout - elapsed return left if left > 0 else 0.0 + def remaining_ms(self) -> float: + """Return milliseconds left before the deadline (never negative).""" + return self.remaining() * 1000 + def expired(self) -> bool: """Return True when no time remains.""" return self.remaining() <= 0 diff --git a/scenarios/cli.py b/scenarios/cli.py index 75881dc..2e51c8f 100644 --- a/scenarios/cli.py +++ b/scenarios/cli.py @@ -79,6 +79,11 @@ def build_parser() -> argparse.ArgumentParser: "location", help="Print catalog location for a scenario id" ) location_parser.add_argument("scenario_id", help="Scenario id from the catalog") + + find_parser = subparsers.add_parser( + "find", help="Find scenarios by id or description substring" + ) + find_parser.add_argument("query", help="Case-insensitive match on id or description") return parser @@ -197,6 +202,20 @@ def main(argv: list[str] | None = None) -> int: print(entry.location) return 0 + if args.command == "find": + query = args.query.casefold() + matches = [ + entry.scenario_id + for entry in CATALOG + 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 + return 1 diff --git a/tests/test_cli.py b/tests/test_cli.py index c780d21..19556a5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -251,5 +251,30 @@ def test_location_unknown_scenario_returns_error(self): self.assertIn("unknown scenario: missing", err.getvalue()) + + def test_find_matches_id_case_insensitive(self): + buffer = io.StringIO() + with redirect_stdout(buffer): + code = main(["find", "CIRCUIT-BREAKER"]) + self.assertEqual(code, 0) + lines = [line for line in buffer.getvalue().splitlines() if line.strip()] + self.assertEqual(lines, ["circuit-breaker"]) + + def test_find_matches_description_case_insensitive(self): + buffer = io.StringIO() + with redirect_stdout(buffer): + code = main(["find", "rate-limit"]) + self.assertEqual(code, 0) + lines = [line for line in buffer.getvalue().splitlines() if line.strip()] + self.assertEqual(lines, ["throttle"]) + + def test_find_no_matches_returns_error(self): + buffer = io.StringIO() + with redirect_stdout(buffer): + code = main(["find", "zzzz-no-such"]) + self.assertEqual(code, 1) + self.assertEqual(buffer.getvalue().strip(), "") + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_deadline.py b/tests/test_deadline.py index ea8d3ac..7890cdf 100644 --- a/tests/test_deadline.py +++ b/tests/test_deadline.py @@ -85,5 +85,21 @@ def test_remaining_fraction_zero_timeout_is_zero(self): self.assertEqual(deadline.remaining_fraction(), 0.0) + + def test_remaining_ms_scales_remaining(self): + clock = FakeClock() + deadline = Deadline(clock, timeout=1.0) + self.assertAlmostEqual(deadline.remaining_ms(), 1000.0) + clock.sleep(0.4) + self.assertAlmostEqual(deadline.remaining_ms(), 600.0) + self.assertFalse(deadline.expired()) + + def test_remaining_ms_zero_when_expired(self): + clock = FakeClock() + deadline = Deadline(clock, timeout=0.5) + clock.sleep(0.5) + self.assertEqual(deadline.remaining_ms(), 0.0) + + if __name__ == "__main__": unittest.main()