diff --git a/pyproject.toml b/pyproject.toml index 9bd3c394..16e964ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "pyyaml", "schema", "rich", + "randomname", "typing_extensions; python_version < '3.11'", ] @@ -141,6 +142,10 @@ exclude = [ "src/canary_gitlab/tests/*", ] +[[tool.mypy.overrides]] +module = "randomname.*" +ignore_missing_imports = true + [[tool.mypy.overrides]] module = "requests.*" ignore_missing_imports = true diff --git a/src/_canary/plugins/subcommands/selection.py b/src/_canary/plugins/subcommands/selection.py index 4a27a701..6ab67700 100644 --- a/src/_canary/plugins/subcommands/selection.py +++ b/src/_canary/plugins/subcommands/selection.py @@ -49,7 +49,7 @@ def execute(self, args: "argparse.Namespace") -> int: raise ValueError( logging.colorize( f"Selection {args.tag!r} already exists, run " - f"[bold]canary refresh {args.tag}[/] to regnerate specs" + f"[bold]canary selection refresh {args.tag}[/] to regnerate specs" ) ) if not args.scanpaths: diff --git a/src/_canary/util/names.py b/src/_canary/util/names.py new file mode 100644 index 00000000..1b054116 --- /dev/null +++ b/src/_canary/util/names.py @@ -0,0 +1,34 @@ +# Copyright NTESS. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT +from typing import Any +from typing import Iterable + +import randomname + +default_groups = ["a/colors", "n/apex_predators"] + + +def random_name(groups: Any = default_groups, seed: int | None = None) -> str: + """Generate a random name with one random entry from each of the provided groups""" + return randomname.generate(*groups, seed=seed) + + +def unique_random_name( + existing_names: Iterable[str], + max_samples: int = 20, + groups: Any = default_groups, + seed: int | None = None, +) -> str: + """Attempt to generate a random name that is not in `existing_names` within `max_samples`. + + Raises `ValueError` if unable to generate a unique name + """ + for _ in range(max_samples): + name = random_name(groups=groups, seed=seed) + if name not in existing_names: + return name + else: + raise ValueError( + f"unable to generate name outside {existing_names} in {max_samples} attempts" + ) diff --git a/src/_canary/workspace.py b/src/_canary/workspace.py index a381c01a..98c139c5 100644 --- a/src/_canary/workspace.py +++ b/src/_canary/workspace.py @@ -42,6 +42,7 @@ from .util.filesystem import write_directory_tag from .util.graph import TopologicalSorter from .util.graph import static_order +from .util.names import unique_random_name from .version import __static_version__ logger = logging.get_logger(__name__) @@ -403,7 +404,7 @@ def create_selection( regex: str | None = None, ) -> list["ResolvedSpec"]: """Find test case generators in scan_paths and add them to this workspace""" - tag = tag or datetime.datetime.now().isoformat(timespec="microseconds") + tag = tag or unique_random_name(self.db.tags) collector = Collector() collector.add_scanpaths(scanpaths) generators = collector.run() @@ -429,6 +430,7 @@ def create_selection( owners=owners, regex=regex, ) + logger.info(f"Created selection '[bold]{tag}[/]'") return specs def apply_selection_rules( diff --git a/tests/util/names_tests.py b/tests/util/names_tests.py new file mode 100644 index 00000000..0c6b5949 --- /dev/null +++ b/tests/util/names_tests.py @@ -0,0 +1,44 @@ +# Copyright NTESS. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT +from _canary.util import names + + +def test_random_name_with_seed_is_repeatable(): + seed = 1 + assert len({names.random_name(seed=seed) for _ in range(10)}) == 1 + + +def test_random_name_does_not_repeat_for_small_count(): + N = 30 + # set the random number generator seed so this test is repeatable + names.random_name(seed=1) + random_names = {names.random_name() for _ in range(N)} + print(random_names) + assert len(random_names) == N + + +def test_unique_random_name_raises_if_unable_to_generate_name(): + seed = 1 + existing_names = {names.random_name(seed=seed)} + try: + _ = names.unique_random_name(existing_names, seed=seed) + assert False + except ValueError as e: + print(e) + assert True + except: + assert False + + +def test_unique_random_name(): + seed = 1 + N = 20 + names.random_name(seed=seed) + existing_names = {names.random_name() for _ in range(N)} + + # reset the seed + names.random_name(seed=seed) + unique_name = names.unique_random_name(existing_names=existing_names, max_samples=N + 1) + + assert unique_name not in existing_names