diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ab1d4ba..c42c92f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [0.0.6] - 2023-12-10 + ### Added - `init` command to create a new config file @@ -14,13 +16,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `implode` command to remove configuration - `sniff` command to inspect configuration and issues - More confirmations for exceptional cases -- A start to unit tests +- Unit tests ### Changed - Move root options to new `sniff` command - Move subcommands and utilities to individual modules - Updated error and confirmation messaging +- Exit with status code 1 in more cases - Open long repo lists in pager ## [0.0.5] - 2023-12-09 diff --git a/setup.cfg b/setup.cfg index 01366d1..7f6124d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = repo-man -version = 0.0.5 +version = 0.0.6 description = Manage repositories of different flavors. long_description = file: README.md long_description_content_type = text/markdown @@ -73,7 +73,7 @@ source = repo_man branch = True [coverage:report] -fail_under = 0.00 +fail_under = 90.00 show_missing = True skip_covered = True @@ -87,6 +87,8 @@ envlist = py39,py312 isolated_build = True [testenv] +package = wheel +wheel_build_env = .pkg deps = pytest pytest-cov diff --git a/src/repo_man/cli.py b/src/repo_man/cli.py index 8448fea..5123b9c 100644 --- a/src/repo_man/cli.py +++ b/src/repo_man/cli.py @@ -15,7 +15,7 @@ @click.group(context_settings={"help_option_names": ["-h", "--help"]}) @click.version_option(package_name="repo-man") @click.pass_context -def cli(context): +def cli(context): # pragma: no cover """Manage repositories of different types""" config = configparser.ConfigParser() @@ -23,7 +23,7 @@ def cli(context): context.obj = config -def main(): +def main(): # pragma: no cover cli.add_command(add) cli.add_command(edit) cli.add_command(flavors) diff --git a/src/repo_man/commands/edit.py b/src/repo_man/commands/edit.py index 6c20a89..8ec2c08 100644 --- a/src/repo_man/commands/edit.py +++ b/src/repo_man/commands/edit.py @@ -11,6 +11,6 @@ def edit(): if not Path(REPO_TYPES_CFG).exists(): click.echo(click.style(f"No {REPO_TYPES_CFG} file found.", fg="red")) - return + raise SystemExit(1) click.edit(filename=REPO_TYPES_CFG) diff --git a/src/repo_man/commands/flavors.py b/src/repo_man/commands/flavors.py index c39e5da..c7aaecc 100644 --- a/src/repo_man/commands/flavors.py +++ b/src/repo_man/commands/flavors.py @@ -15,7 +15,7 @@ def flavors(config: configparser.ConfigParser, repo: str): if not Path(REPO_TYPES_CFG).exists(): click.echo(click.style(f"No {REPO_TYPES_CFG} file found.", fg="red")) - return + raise SystemExit(1) found = set() diff --git a/src/repo_man/commands/list_repos.py b/src/repo_man/commands/list_repos.py index c327ede..5bcef4e 100644 --- a/src/repo_man/commands/list_repos.py +++ b/src/repo_man/commands/list_repos.py @@ -15,7 +15,7 @@ def list_repos(config: configparser.ConfigParser, repo_type: str): if not Path(REPO_TYPES_CFG).exists(): click.echo(click.style(f"No {REPO_TYPES_CFG} file found.", fg="red")) - return + raise SystemExit(1) valid_repo_types = parse_repo_types(config) diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..888f5cd --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,21 @@ +import configparser + +import pytest +from click.testing import CliRunner + +from repo_man.consts import REPO_TYPES_CFG + + +@pytest.fixture +def runner(): + return CliRunner() + + +@pytest.fixture +def get_config(): + def func(): + config = configparser.ConfigParser() + config.read(REPO_TYPES_CFG) + return config + + return func diff --git a/test/test_add.py b/test/test_add.py index 4b0ec8f..99c04b8 100644 --- a/test/test_add.py +++ b/test/test_add.py @@ -1,18 +1,12 @@ -import configparser from pathlib import Path -from click.testing import CliRunner - from repo_man.commands.add import add -def test_add_clean_confirm(): - runner = CliRunner() - +def test_add_clean_confirm(runner, get_config): with runner.isolated_filesystem(): (Path(".") / "some-repo").mkdir() - config = configparser.ConfigParser() - config.read("repo-man.cfg") + config = get_config() result = runner.invoke(add, ["some-repo", "-t", "some-type"], input="Y\nY\n", obj=config) assert result.exit_code == 0 assert ( @@ -37,13 +31,10 @@ def test_add_clean_confirm(): ) -def test_add_clean_no_confirm_new_file(): - runner = CliRunner() - +def test_add_clean_no_confirm_new_file(runner, get_config): with runner.isolated_filesystem(): (Path(".") / "some-repo").mkdir() - config = configparser.ConfigParser() - config.read("repo-man.cfg") + config = get_config() result = runner.invoke(add, ["some-repo", "-t", "some-type"], input="\n", obj=config) assert result.exit_code == 1 assert ( @@ -54,9 +45,7 @@ def test_add_clean_no_confirm_new_file(): ) -def test_add_with_existing_file(): - runner = CliRunner() - +def test_add_with_existing_file(runner, get_config): with runner.isolated_filesystem(): with open("repo-man.cfg", "w") as config_file: config_file.write( @@ -67,8 +56,7 @@ def test_add_with_existing_file(): ) (Path(".") / "some-repo").mkdir() - config = configparser.ConfigParser() - config.read("repo-man.cfg") + config = get_config() result = runner.invoke(add, ["some-repo", "-t", "some-type"], input="Y\n", obj=config) assert result.exit_code == 0 assert ( @@ -96,9 +84,7 @@ def test_add_with_existing_file(): ) -def test_add_with_existing_file_and_type(): - runner = CliRunner() - +def test_add_with_existing_file_and_type(runner, get_config): with runner.isolated_filesystem(): with open("repo-man.cfg", "w") as config_file: config_file.write( @@ -109,8 +95,7 @@ def test_add_with_existing_file_and_type(): ) (Path(".") / "some-repo").mkdir() - config = configparser.ConfigParser() - config.read("repo-man.cfg") + config = get_config() result = runner.invoke(add, ["some-repo", "-t", "some-type"], obj=config) assert result.exit_code == 0 assert result.output == "" @@ -127,9 +112,7 @@ def test_add_with_existing_file_and_type(): ) -def test_add_multiple_types(): - runner = CliRunner() - +def test_add_multiple_types(runner, get_config): with runner.isolated_filesystem(): with open("repo-man.cfg", "w") as config_file: config_file.write( @@ -140,8 +123,7 @@ def test_add_multiple_types(): ) (Path(".") / "some-repo").mkdir() - config = configparser.ConfigParser() - config.read("repo-man.cfg") + config = get_config() result = runner.invoke(add, ["some-repo", "-t", "some-type", "-t", "some-other-type"], input="Y\n", obj=config) assert result.exit_code == 0 assert ( @@ -170,9 +152,7 @@ def test_add_multiple_types(): ) -def test_add_no_action_needed(): - runner = CliRunner() - +def test_add_no_action_needed(runner, get_config): with runner.isolated_filesystem(): with open("repo-man.cfg", "w") as config_file: config_file.write( @@ -183,8 +163,7 @@ def test_add_no_action_needed(): ) (Path(".") / "some-repo").mkdir() - config = configparser.ConfigParser() - config.read("repo-man.cfg") + config = get_config() result = runner.invoke(add, ["some-repo", "-t", "some-type"], obj=config) assert result.exit_code == 0 assert result.output == "" diff --git a/test/test_edit.py b/test/test_edit.py new file mode 100644 index 0000000..dba0bb9 --- /dev/null +++ b/test/test_edit.py @@ -0,0 +1,21 @@ +from pathlib import Path +from unittest.mock import patch + +from repo_man.commands import edit + + +def test_edit_clean(runner): + with runner.isolated_filesystem(): + result = runner.invoke(edit.edit) + assert result.exit_code == 1 + assert result.output == "No repo-man.cfg file found.\n" + + +@patch("repo_man.commands.edit.click.edit") +def test_edit_when_config_present(mock_edit, runner): + with runner.isolated_filesystem(): + Path("repo-man.cfg").touch() + result = runner.invoke(edit.edit) + assert result.exit_code == 0 + assert result.output == "" + mock_edit.assert_called_once_with(filename="repo-man.cfg") diff --git a/test/test_flavors.py b/test/test_flavors.py new file mode 100644 index 0000000..b6663fe --- /dev/null +++ b/test/test_flavors.py @@ -0,0 +1,73 @@ +from pathlib import Path + +from repo_man.commands.flavors import flavors + + +def test_flavors_clean(runner, get_config): + with runner.isolated_filesystem(): + Path("some-repo").mkdir() + config = get_config() + result = runner.invoke(flavors, ["some-repo"], obj=config) + assert result.exit_code == 1 + assert result.output == "No repo-man.cfg file found.\n" + + +def test_flavors_when_configured(runner, get_config): + with runner.isolated_filesystem(): + Path("some-repo").mkdir() + + with open("repo-man.cfg", "w") as config_file: + config_file.write( + """[foo] +known = + some-repo + +[ignore] +known = + some-other-repo + +""" + ) + + config = get_config() + result = runner.invoke(flavors, ["some-repo"], obj=config) + assert result.exit_code == 0 + assert result.output == "foo\n" + + +def test_flavors_when_not_configured(runner, get_config): + with runner.isolated_filesystem(): + Path("some-repo").mkdir() + + with open("repo-man.cfg", "w") as config_file: + config_file.write( + """[foo] +known = + some-other-repo + +""" + ) + + config = get_config() + result = runner.invoke(flavors, ["some-repo"], obj=config) + assert result.exit_code == 0 + assert result.output == "" + + +def test_flavors_when_ignored(runner, get_config): + with runner.isolated_filesystem(): + Path("some-repo").mkdir() + + with open("repo-man.cfg", "w") as config_file: + config_file.write( + """[ignore] +known = + some-repo + +""" + ) + + config = get_config() + result = runner.invoke(flavors, ["some-repo"], obj=config) + assert result.exit_code == 0 + assert result.output == "" diff --git a/test/test_implode.py b/test/test_implode.py new file mode 100644 index 0000000..578dd77 --- /dev/null +++ b/test/test_implode.py @@ -0,0 +1,35 @@ +from pathlib import Path + +from repo_man.commands.implode import implode + + +def test_implode_when_config_present_confirm(runner): + with runner.isolated_filesystem(): + Path("repo-man.cfg").touch() + + result = runner.invoke(implode, ["."], input="Y\n") + assert result.exit_code == 0 + assert result.output == "Are you sure you want to do this? [y/N]: Y\n" + assert not Path("repo-man.cfg").exists() + + +def test_implode_when_config_not_present_confirm(runner): + with runner.isolated_filesystem(): + result = runner.invoke(implode, ["."], input="Y\n") + assert result.exit_code == 0 + assert result.output == "Are you sure you want to do this? [y/N]: Y\n" + + +def test_implode_when_config_present_no_confirm(runner): + with runner.isolated_filesystem(): + Path("repo-man.cfg").touch() + + result = runner.invoke(implode, ["."], input="\n") + assert result.exit_code == 1 + assert ( + result.output + == """Are you sure you want to do this? [y/N]: +Aborted! +""" + ) + assert Path("repo-man.cfg").exists() diff --git a/test/test_init.py b/test/test_init.py index d3726e2..811d4dc 100644 --- a/test/test_init.py +++ b/test/test_init.py @@ -1,13 +1,9 @@ from pathlib import Path -from click.testing import CliRunner - from repo_man.cli import init -def test_init_clean(): - runner = CliRunner() - +def test_init_clean(runner): with runner.isolated_filesystem(): assert not Path("repo-man.cfg").exists() @@ -17,9 +13,7 @@ def test_init_clean(): assert Path("repo-man.cfg").exists() -def test_init_with_existing_confirm(): - runner = CliRunner() - +def test_init_with_existing_confirm(runner): with runner.isolated_filesystem(): with open("repo-man.cfg", "w") as config_file: config_file.write( @@ -37,9 +31,7 @@ def test_init_with_existing_confirm(): assert config_file.read() == "" -def test_init_with_existing_no_confirm(): - runner = CliRunner() - +def test_init_with_existing_no_confirm(runner): with runner.isolated_filesystem(): with open("repo-man.cfg", "w") as config_file: config_file.write( diff --git a/test/test_list_repos.py b/test/test_list_repos.py new file mode 100644 index 0000000..21bf1c2 --- /dev/null +++ b/test/test_list_repos.py @@ -0,0 +1,81 @@ +from unittest.mock import patch + +from repo_man.commands.list_repos import list_repos + + +def test_list_repos_clean(runner, get_config): + with runner.isolated_filesystem(): + config = get_config() + result = runner.invoke(list_repos, ["-t", "all"], obj=config) + assert result.exit_code == 1 + assert result.output == "No repo-man.cfg file found.\n" + + +def test_list_repos_with_matches(runner, get_config): + with runner.isolated_filesystem(): + with open("repo-man.cfg", "w") as config_file: + config_file.write( + """[foo] +known = + some-repo + some-other-repo + +""" + ) + + config = get_config() + result = runner.invoke(list_repos, ["-t", "all"], obj=config) + assert result.exit_code == 0 + assert ( + result.output + == """some-other-repo +some-repo +""" + ) + + +@patch("repo_man.commands.list_repos.click.echo_via_pager") +def test_list_repos_when_long(mock_echo_via_pager, runner, get_config): + all_repos = """some-repo-1 +some-repo-2 +some-repo-3 +some-repo-4 +some-repo-5 +some-repo-6 +some-repo-7 +some-repo-8 +some-repo-9 +some-repo-10 +some-repo-11 +some-repo-12 +some-repo-13 +some-repo-14 +some-repo-15 +some-repo-16 +some-repo-17 +some-repo-18 +some-repo-19 +some-repo-20 +some-repo-21 +some-repo-22 +some-repo-23 +some-repo-24 +some-repo-25 +some-repo-26""" + + config_list = "\n ".join(all_repos.split("\n")) + + with runner.isolated_filesystem(): + with open("repo-man.cfg", "w") as config_file: + config_file.write( + f"""[foo] +known = + {config_list} + +""" + ) + + config = get_config() + result = runner.invoke(list_repos, ["-t", "all"], obj=config) + assert result.exit_code == 0 + mock_echo_via_pager.assert_called_once_with("\n".join(sorted(all_repos.split("\n")))) diff --git a/test/test_sniff.py b/test/test_sniff.py new file mode 100644 index 0000000..27fcfea --- /dev/null +++ b/test/test_sniff.py @@ -0,0 +1,77 @@ +from pathlib import Path + +from repo_man.commands.sniff import sniff + + +def test_known(runner, get_config): + with runner.isolated_filesystem(): + with open("repo-man.cfg", "w") as config_file: + config_file.write( + """[foo] +known = + some-repo + +[bar] +known = + some-other-repo + +[ignore] +known = + yet-another-repo + +""" + ) + + config = get_config() + config.read("repo-man.cfg") + + result = runner.invoke(sniff, ["--known"], obj=config) + assert result.exit_code == 0 + assert result.output == "bar\nfoo\n" + + +def test_unconfigured(runner, get_config): + with runner.isolated_filesystem(): + Path("some-repo").mkdir() + Path("some-other-repo").mkdir() + with open("repo-man.cfg", "w") as config_file: + config_file.write( + """[foo] +known = + some-repo + +""" + ) + + config = get_config() + config.read("repo-man.cfg") + + result = runner.invoke(sniff, ["--unconfigured"], obj=config) + assert result.exit_code == 0 + assert result.output == "some-other-repo\n" + + +def test_duplicates(runner, get_config): + with runner.isolated_filesystem(): + with open("repo-man.cfg", "w") as config_file: + config_file.write( + """[foo] +known = + some-repo + some-other-repo + +[bar] +known = + some-repo + some-other-repo + yet-another-repo + +""" + ) + + config = get_config() + config.read("repo-man.cfg") + + result = runner.invoke(sniff, ["--duplicates"], obj=config) + assert result.exit_code == 0 + assert result.output == "some-other-repo\nsome-repo\n"