diff options
| author | Mike Frysinger <vapier@google.com> | 2026-03-18 11:17:12 -0400 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2026-03-18 11:13:26 -0700 |
| commit | f24bc7aed59a8fb4b89afa06b82eae1aad067983 (patch) | |
| tree | 8d9c02a8296910ea4126574112c667cdeb2bc6af /tests | |
| parent | 83b8ebdbbed5ec894a2f89eb5e4bcf5d0bb14300 (diff) | |
| download | git-repo-f24bc7aed59a8fb4b89afa06b82eae1aad067983.tar.gz | |
tests: switch some test modules to pytest
Change-Id: I524b5ff2d77f8232f94e21921b00ba4027d2ac4f
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/563081
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_color.py | 102 | ||||
| -rw-r--r-- | tests/test_editor.py | 47 | ||||
| -rw-r--r-- | tests/test_error.py | 65 | ||||
| -rw-r--r-- | tests/test_git_config.py | 397 | ||||
| -rw-r--r-- | tests/test_hooks.py | 75 | ||||
| -rw-r--r-- | tests/test_platform_utils.py | 50 | ||||
| -rw-r--r-- | tests/test_repo_logging.py | 148 | ||||
| -rw-r--r-- | tests/test_repo_trace.py | 67 | ||||
| -rw-r--r-- | tests/test_ssh.py | 134 | ||||
| -rw-r--r-- | tests/test_subcmds.py | 264 | ||||
| -rw-r--r-- | tests/test_subcmds_init.py | 55 | ||||
| -rw-r--r-- | tests/test_update_manpages.py | 13 |
12 files changed, 717 insertions, 700 deletions
diff --git a/tests/test_color.py b/tests/test_color.py index 91a1bf250..923f7e355 100644 --- a/tests/test_color.py +++ b/tests/test_color.py | |||
| @@ -15,60 +15,66 @@ | |||
| 15 | """Unittests for the color.py module.""" | 15 | """Unittests for the color.py module.""" |
| 16 | 16 | ||
| 17 | import os | 17 | import os |
| 18 | import unittest | 18 | |
| 19 | import pytest | ||
| 19 | 20 | ||
| 20 | import color | 21 | import color |
| 21 | import git_config | 22 | import git_config |
| 22 | 23 | ||
| 23 | 24 | ||
| 24 | def fixture(*paths): | 25 | def fixture(*paths: str) -> str: |
| 25 | """Return a path relative to test/fixtures.""" | 26 | """Return a path relative to test/fixtures.""" |
| 26 | return os.path.join(os.path.dirname(__file__), "fixtures", *paths) | 27 | return os.path.join(os.path.dirname(__file__), "fixtures", *paths) |
| 27 | 28 | ||
| 28 | 29 | ||
| 29 | class ColoringTests(unittest.TestCase): | 30 | @pytest.fixture |
| 30 | """tests of the Coloring class.""" | 31 | def coloring() -> color.Coloring: |
| 31 | 32 | """Create a Coloring object for testing.""" | |
| 32 | def setUp(self): | 33 | config_fixture = fixture("test.gitconfig") |
| 33 | """Create a GitConfig object using the test.gitconfig fixture.""" | 34 | config = git_config.GitConfig(config_fixture) |
| 34 | config_fixture = fixture("test.gitconfig") | 35 | color.SetDefaultColoring("true") |
| 35 | self.config = git_config.GitConfig(config_fixture) | 36 | return color.Coloring(config, "status") |
| 36 | color.SetDefaultColoring("true") | 37 | |
| 37 | self.color = color.Coloring(self.config, "status") | 38 | |
| 38 | 39 | def test_Color_Parse_all_params_none(coloring: color.Coloring) -> None: | |
| 39 | def test_Color_Parse_all_params_none(self): | 40 | """all params are None""" |
| 40 | """all params are None""" | 41 | val = coloring._parse(None, None, None, None) |
| 41 | val = self.color._parse(None, None, None, None) | 42 | assert val == "" |
| 42 | self.assertEqual("", val) | 43 | |
| 43 | 44 | ||
| 44 | def test_Color_Parse_first_parameter_none(self): | 45 | def test_Color_Parse_first_parameter_none(coloring: color.Coloring) -> None: |
| 45 | """check fg & bg & attr""" | 46 | """check fg & bg & attr""" |
| 46 | val = self.color._parse(None, "black", "red", "ul") | 47 | val = coloring._parse(None, "black", "red", "ul") |
| 47 | self.assertEqual("\x1b[4;30;41m", val) | 48 | assert val == "\x1b[4;30;41m" |
| 48 | 49 | ||
| 49 | def test_Color_Parse_one_entry(self): | 50 | |
| 50 | """check fg""" | 51 | def test_Color_Parse_one_entry(coloring: color.Coloring) -> None: |
| 51 | val = self.color._parse("one", None, None, None) | 52 | """check fg""" |
| 52 | self.assertEqual("\033[33m", val) | 53 | val = coloring._parse("one", None, None, None) |
| 53 | 54 | assert val == "\033[33m" | |
| 54 | def test_Color_Parse_two_entry(self): | 55 | |
| 55 | """check fg & bg""" | 56 | |
| 56 | val = self.color._parse("two", None, None, None) | 57 | def test_Color_Parse_two_entry(coloring: color.Coloring) -> None: |
| 57 | self.assertEqual("\033[35;46m", val) | 58 | """check fg & bg""" |
| 58 | 59 | val = coloring._parse("two", None, None, None) | |
| 59 | def test_Color_Parse_three_entry(self): | 60 | assert val == "\033[35;46m" |
| 60 | """check fg & bg & attr""" | 61 | |
| 61 | val = self.color._parse("three", None, None, None) | 62 | |
| 62 | self.assertEqual("\033[4;30;41m", val) | 63 | def test_Color_Parse_three_entry(coloring: color.Coloring) -> None: |
| 63 | 64 | """check fg & bg & attr""" | |
| 64 | def test_Color_Parse_reset_entry(self): | 65 | val = coloring._parse("three", None, None, None) |
| 65 | """check reset entry""" | 66 | assert val == "\033[4;30;41m" |
| 66 | val = self.color._parse("reset", None, None, None) | 67 | |
| 67 | self.assertEqual("\033[m", val) | 68 | |
| 68 | 69 | def test_Color_Parse_reset_entry(coloring: color.Coloring) -> None: | |
| 69 | def test_Color_Parse_empty_entry(self): | 70 | """check reset entry""" |
| 70 | """check empty entry""" | 71 | val = coloring._parse("reset", None, None, None) |
| 71 | val = self.color._parse("none", "blue", "white", "dim") | 72 | assert val == "\033[m" |
| 72 | self.assertEqual("\033[2;34;47m", val) | 73 | |
| 73 | val = self.color._parse("empty", "green", "white", "bold") | 74 | |
| 74 | self.assertEqual("\033[1;32;47m", val) | 75 | def test_Color_Parse_empty_entry(coloring: color.Coloring) -> None: |
| 76 | """check empty entry""" | ||
| 77 | val = coloring._parse("none", "blue", "white", "dim") | ||
| 78 | assert val == "\033[2;34;47m" | ||
| 79 | val = coloring._parse("empty", "green", "white", "bold") | ||
| 80 | assert val == "\033[1;32;47m" | ||
diff --git a/tests/test_editor.py b/tests/test_editor.py index 8f5d160e2..d51765dd1 100644 --- a/tests/test_editor.py +++ b/tests/test_editor.py | |||
| @@ -14,43 +14,32 @@ | |||
| 14 | 14 | ||
| 15 | """Unittests for the editor.py module.""" | 15 | """Unittests for the editor.py module.""" |
| 16 | 16 | ||
| 17 | import unittest | 17 | import pytest |
| 18 | 18 | ||
| 19 | from editor import Editor | 19 | from editor import Editor |
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | class EditorTestCase(unittest.TestCase): | 22 | @pytest.fixture(autouse=True) |
| 23 | def reset_editor() -> None: | ||
| 23 | """Take care of resetting Editor state across tests.""" | 24 | """Take care of resetting Editor state across tests.""" |
| 25 | Editor._editor = None | ||
| 26 | yield | ||
| 27 | Editor._editor = None | ||
| 24 | 28 | ||
| 25 | def setUp(self): | ||
| 26 | self.setEditor(None) | ||
| 27 | 29 | ||
| 28 | def tearDown(self): | 30 | def test_basic() -> None: |
| 29 | self.setEditor(None) | 31 | """Basic checking of _GetEditor.""" |
| 32 | Editor._editor = ":" | ||
| 33 | assert Editor._GetEditor() == ":" | ||
| 30 | 34 | ||
| 31 | @staticmethod | ||
| 32 | def setEditor(editor): | ||
| 33 | Editor._editor = editor | ||
| 34 | 35 | ||
| 36 | def test_no_editor() -> None: | ||
| 37 | """Check behavior when no editor is available.""" | ||
| 38 | Editor._editor = ":" | ||
| 39 | assert Editor.EditString("foo") == "foo" | ||
| 35 | 40 | ||
| 36 | class GetEditor(EditorTestCase): | ||
| 37 | """Check GetEditor behavior.""" | ||
| 38 | 41 | ||
| 39 | def test_basic(self): | 42 | def test_cat_editor() -> None: |
| 40 | """Basic checking of _GetEditor.""" | 43 | """Check behavior when editor is `cat`.""" |
| 41 | self.setEditor(":") | 44 | Editor._editor = "cat" |
| 42 | self.assertEqual(":", Editor._GetEditor()) | 45 | assert Editor.EditString("foo") == "foo" |
| 43 | |||
| 44 | |||
| 45 | class EditString(EditorTestCase): | ||
| 46 | """Check EditString behavior.""" | ||
| 47 | |||
| 48 | def test_no_editor(self): | ||
| 49 | """Check behavior when no editor is available.""" | ||
| 50 | self.setEditor(":") | ||
| 51 | self.assertEqual("foo", Editor.EditString("foo")) | ||
| 52 | |||
| 53 | def test_cat_editor(self): | ||
| 54 | """Check behavior when editor is `cat`.""" | ||
| 55 | self.setEditor("cat") | ||
| 56 | self.assertEqual("foo", Editor.EditString("foo")) | ||
diff --git a/tests/test_error.py b/tests/test_error.py index a0ff32c31..3eb82835e 100644 --- a/tests/test_error.py +++ b/tests/test_error.py | |||
| @@ -16,7 +16,9 @@ | |||
| 16 | 16 | ||
| 17 | import inspect | 17 | import inspect |
| 18 | import pickle | 18 | import pickle |
| 19 | import unittest | 19 | from typing import Iterator, Type |
| 20 | |||
| 21 | import pytest | ||
| 20 | 22 | ||
| 21 | import command | 23 | import command |
| 22 | import error | 24 | import error |
| @@ -26,7 +28,7 @@ import project | |||
| 26 | from subcmds import all_modules | 28 | from subcmds import all_modules |
| 27 | 29 | ||
| 28 | 30 | ||
| 29 | imports = all_modules + [ | 31 | _IMPORTS = all_modules + [ |
| 30 | error, | 32 | error, |
| 31 | project, | 33 | project, |
| 32 | git_command, | 34 | git_command, |
| @@ -35,36 +37,35 @@ imports = all_modules + [ | |||
| 35 | ] | 37 | ] |
| 36 | 38 | ||
| 37 | 39 | ||
| 38 | class PickleTests(unittest.TestCase): | 40 | def get_exceptions() -> Iterator[Type[Exception]]: |
| 39 | """Make sure all our custom exceptions can be pickled.""" | 41 | """Return all our custom exceptions.""" |
| 42 | for entry in _IMPORTS: | ||
| 43 | for name in dir(entry): | ||
| 44 | cls = getattr(entry, name) | ||
| 45 | if isinstance(cls, type) and issubclass(cls, Exception): | ||
| 46 | yield cls | ||
| 47 | |||
| 40 | 48 | ||
| 41 | def getExceptions(self): | 49 | def test_exception_lookup() -> None: |
| 42 | """Return all our custom exceptions.""" | 50 | """Make sure our introspection logic works.""" |
| 43 | for entry in imports: | 51 | classes = list(get_exceptions()) |
| 44 | for name in dir(entry): | 52 | assert error.HookError in classes |
| 45 | cls = getattr(entry, name) | 53 | # Don't assert the exact number to avoid being a change-detector test. |
| 46 | if isinstance(cls, type) and issubclass(cls, Exception): | 54 | assert len(classes) > 10 |
| 47 | yield cls | ||
| 48 | 55 | ||
| 49 | def testExceptionLookup(self): | ||
| 50 | """Make sure our introspection logic works.""" | ||
| 51 | classes = list(self.getExceptions()) | ||
| 52 | self.assertIn(error.HookError, classes) | ||
| 53 | # Don't assert the exact number to avoid being a change-detector test. | ||
| 54 | self.assertGreater(len(classes), 10) | ||
| 55 | 56 | ||
| 56 | def testPickle(self): | 57 | @pytest.mark.parametrize("cls", get_exceptions()) |
| 57 | """Try to pickle all the exceptions.""" | 58 | def test_pickle(cls: Type[Exception]) -> None: |
| 58 | for cls in self.getExceptions(): | 59 | """Try to pickle all the exceptions.""" |
| 59 | args = inspect.getfullargspec(cls.__init__).args[1:] | 60 | args = inspect.getfullargspec(cls.__init__).args[1:] |
| 60 | obj = cls(*args) | 61 | obj = cls(*args) |
| 61 | p = pickle.dumps(obj) | 62 | p = pickle.dumps(obj) |
| 62 | try: | 63 | try: |
| 63 | newobj = pickle.loads(p) | 64 | newobj = pickle.loads(p) |
| 64 | except Exception as e: # pylint: disable=broad-except | 65 | except Exception as e: |
| 65 | self.fail( | 66 | pytest.fail( |
| 66 | "Class %s is unable to be pickled: %s\n" | 67 | f"Class {cls} is unable to be pickled: {e}\n" |
| 67 | "Incomplete super().__init__(...) call?" % (cls, e) | 68 | "Incomplete super().__init__(...) call?" |
| 68 | ) | 69 | ) |
| 69 | self.assertIsInstance(newobj, cls) | 70 | assert isinstance(newobj, cls) |
| 70 | self.assertEqual(str(obj), str(newobj)) | 71 | assert str(obj) == str(newobj) |
diff --git a/tests/test_git_config.py b/tests/test_git_config.py index e1604bd76..496d97141 100644 --- a/tests/test_git_config.py +++ b/tests/test_git_config.py | |||
| @@ -15,200 +15,219 @@ | |||
| 15 | """Unittests for the git_config.py module.""" | 15 | """Unittests for the git_config.py module.""" |
| 16 | 16 | ||
| 17 | import os | 17 | import os |
| 18 | import tempfile | 18 | from pathlib import Path |
| 19 | import unittest | 19 | from typing import Any |
| 20 | |||
| 21 | import pytest | ||
| 20 | 22 | ||
| 21 | import git_config | 23 | import git_config |
| 22 | 24 | ||
| 23 | 25 | ||
| 24 | def fixture(*paths): | 26 | def fixture_path(*paths: str) -> str: |
| 25 | """Return a path relative to test/fixtures.""" | 27 | """Return a path relative to test/fixtures.""" |
| 26 | return os.path.join(os.path.dirname(__file__), "fixtures", *paths) | 28 | return os.path.join(os.path.dirname(__file__), "fixtures", *paths) |
| 27 | 29 | ||
| 28 | 30 | ||
| 29 | class GitConfigReadOnlyTests(unittest.TestCase): | 31 | @pytest.fixture |
| 30 | """Read-only tests of the GitConfig class.""" | 32 | def readonly_config() -> git_config.GitConfig: |
| 31 | 33 | """Create a GitConfig object using the test.gitconfig fixture.""" | |
| 32 | def setUp(self): | 34 | config_fixture = fixture_path("test.gitconfig") |
| 33 | """Create a GitConfig object using the test.gitconfig fixture.""" | 35 | return git_config.GitConfig(config_fixture) |
| 34 | config_fixture = fixture("test.gitconfig") | 36 | |
| 35 | self.config = git_config.GitConfig(config_fixture) | 37 | |
| 36 | 38 | def test_get_string_with_empty_config_values( | |
| 37 | def test_GetString_with_empty_config_values(self): | 39 | readonly_config: git_config.GitConfig, |
| 38 | """ | 40 | ) -> None: |
| 39 | Test config entries with no value. | 41 | """Test config entries with no value. |
| 40 | 42 | ||
| 41 | [section] | 43 | [section] |
| 42 | empty | 44 | empty |
| 43 | 45 | ||
| 44 | """ | 46 | """ |
| 45 | val = self.config.GetString("section.empty") | 47 | val = readonly_config.GetString("section.empty") |
| 46 | self.assertEqual(val, None) | 48 | assert val is None |
| 47 | 49 | ||
| 48 | def test_GetString_with_true_value(self): | 50 | |
| 49 | """ | 51 | def test_get_string_with_true_value( |
| 50 | Test config entries with a string value. | 52 | readonly_config: git_config.GitConfig, |
| 51 | 53 | ) -> None: | |
| 52 | [section] | 54 | """Test config entries with a string value. |
| 53 | nonempty = true | 55 | |
| 54 | 56 | [section] | |
| 55 | """ | 57 | nonempty = true |
| 56 | val = self.config.GetString("section.nonempty") | 58 | |
| 57 | self.assertEqual(val, "true") | 59 | """ |
| 58 | 60 | val = readonly_config.GetString("section.nonempty") | |
| 59 | def test_GetString_from_missing_file(self): | 61 | assert val == "true" |
| 60 | """ | 62 | |
| 61 | Test missing config file | 63 | |
| 62 | """ | 64 | def test_get_string_from_missing_file() -> None: |
| 63 | config_fixture = fixture("not.present.gitconfig") | 65 | """Test missing config file.""" |
| 64 | config = git_config.GitConfig(config_fixture) | 66 | config_fixture = fixture_path("not.present.gitconfig") |
| 65 | val = config.GetString("empty") | 67 | config = git_config.GitConfig(config_fixture) |
| 66 | self.assertEqual(val, None) | 68 | val = config.GetString("empty") |
| 67 | 69 | assert val is None | |
| 68 | def test_GetBoolean_undefined(self): | 70 | |
| 69 | """Test GetBoolean on key that doesn't exist.""" | 71 | |
| 70 | self.assertIsNone(self.config.GetBoolean("section.missing")) | 72 | def test_get_boolean_undefined(readonly_config: git_config.GitConfig) -> None: |
| 71 | 73 | """Test GetBoolean on key that doesn't exist.""" | |
| 72 | def test_GetBoolean_invalid(self): | 74 | assert readonly_config.GetBoolean("section.missing") is None |
| 73 | """Test GetBoolean on invalid boolean value.""" | 75 | |
| 74 | self.assertIsNone(self.config.GetBoolean("section.boolinvalid")) | 76 | |
| 75 | 77 | def test_get_boolean_invalid(readonly_config: git_config.GitConfig) -> None: | |
| 76 | def test_GetBoolean_true(self): | 78 | """Test GetBoolean on invalid boolean value.""" |
| 77 | """Test GetBoolean on valid true boolean.""" | 79 | assert readonly_config.GetBoolean("section.boolinvalid") is None |
| 78 | self.assertTrue(self.config.GetBoolean("section.booltrue")) | 80 | |
| 79 | 81 | ||
| 80 | def test_GetBoolean_false(self): | 82 | def test_get_boolean_true(readonly_config: git_config.GitConfig) -> None: |
| 81 | """Test GetBoolean on valid false boolean.""" | 83 | """Test GetBoolean on valid true boolean.""" |
| 82 | self.assertFalse(self.config.GetBoolean("section.boolfalse")) | 84 | assert readonly_config.GetBoolean("section.booltrue") is True |
| 83 | 85 | ||
| 84 | def test_GetInt_undefined(self): | 86 | |
| 85 | """Test GetInt on key that doesn't exist.""" | 87 | def test_get_boolean_false(readonly_config: git_config.GitConfig) -> None: |
| 86 | self.assertIsNone(self.config.GetInt("section.missing")) | 88 | """Test GetBoolean on valid false boolean.""" |
| 87 | 89 | assert readonly_config.GetBoolean("section.boolfalse") is False | |
| 88 | def test_GetInt_invalid(self): | 90 | |
| 89 | """Test GetInt on invalid integer value.""" | 91 | |
| 90 | self.assertIsNone(self.config.GetBoolean("section.intinvalid")) | 92 | def test_get_int_undefined(readonly_config: git_config.GitConfig) -> None: |
| 91 | 93 | """Test GetInt on key that doesn't exist.""" | |
| 92 | def test_GetInt_valid(self): | 94 | assert readonly_config.GetInt("section.missing") is None |
| 93 | """Test GetInt on valid integers.""" | 95 | |
| 94 | TESTS = ( | 96 | |
| 95 | ("inthex", 16), | 97 | def test_get_int_invalid(readonly_config: git_config.GitConfig) -> None: |
| 96 | ("inthexk", 16384), | 98 | """Test GetInt on invalid integer value.""" |
| 97 | ("int", 10), | 99 | assert readonly_config.GetInt("section.intinvalid") is None |
| 98 | ("intk", 10240), | 100 | |
| 99 | ("intm", 10485760), | 101 | |
| 100 | ("intg", 10737418240), | 102 | @pytest.mark.parametrize( |
| 101 | ) | 103 | "key, expected", |
| 102 | for key, value in TESTS: | 104 | ( |
| 103 | self.assertEqual(value, self.config.GetInt(f"section.{key}")) | 105 | ("inthex", 16), |
| 104 | 106 | ("inthexk", 16384), | |
| 105 | 107 | ("int", 10), | |
| 106 | class GitConfigReadWriteTests(unittest.TestCase): | 108 | ("intk", 10240), |
| 107 | """Read/write tests of the GitConfig class.""" | 109 | ("intm", 10485760), |
| 108 | 110 | ("intg", 10737418240), | |
| 109 | def setUp(self): | 111 | ), |
| 110 | self.tmpfile = tempfile.NamedTemporaryFile() | 112 | ) |
| 111 | self.config = self.get_config() | 113 | def test_get_int_valid( |
| 112 | 114 | readonly_config: git_config.GitConfig, key: str, expected: int | |
| 113 | def get_config(self): | 115 | ) -> None: |
| 114 | """Get a new GitConfig instance.""" | 116 | """Test GetInt on valid integers.""" |
| 115 | return git_config.GitConfig(self.tmpfile.name) | 117 | assert readonly_config.GetInt(f"section.{key}") == expected |
| 116 | 118 | ||
| 117 | def test_SetString(self): | 119 | |
| 118 | """Test SetString behavior.""" | 120 | @pytest.fixture |
| 119 | # Set a value. | 121 | def rw_config_file(tmp_path: Path) -> Path: |
| 120 | self.assertIsNone(self.config.GetString("foo.bar")) | 122 | """Return a path to a temporary config file.""" |
| 121 | self.config.SetString("foo.bar", "val") | 123 | return tmp_path / "config" |
| 122 | self.assertEqual("val", self.config.GetString("foo.bar")) | 124 | |
| 123 | 125 | ||
| 124 | # Make sure the value was actually written out. | 126 | def test_set_string(rw_config_file: Path) -> None: |
| 125 | config = self.get_config() | 127 | """Test SetString behavior.""" |
| 126 | self.assertEqual("val", config.GetString("foo.bar")) | 128 | config = git_config.GitConfig(str(rw_config_file)) |
| 127 | 129 | ||
| 128 | # Update the value. | 130 | # Set a value. |
| 129 | self.config.SetString("foo.bar", "valll") | 131 | assert config.GetString("foo.bar") is None |
| 130 | self.assertEqual("valll", self.config.GetString("foo.bar")) | 132 | config.SetString("foo.bar", "val") |
| 131 | config = self.get_config() | 133 | assert config.GetString("foo.bar") == "val" |
| 132 | self.assertEqual("valll", config.GetString("foo.bar")) | 134 | |
| 133 | 135 | # Make sure the value was actually written out. | |
| 134 | # Delete the value. | 136 | config2 = git_config.GitConfig(str(rw_config_file)) |
| 135 | self.config.SetString("foo.bar", None) | 137 | assert config2.GetString("foo.bar") == "val" |
| 136 | self.assertIsNone(self.config.GetString("foo.bar")) | 138 | |
| 137 | config = self.get_config() | 139 | # Update the value. |
| 138 | self.assertIsNone(config.GetString("foo.bar")) | 140 | config.SetString("foo.bar", "valll") |
| 139 | 141 | assert config.GetString("foo.bar") == "valll" | |
| 140 | def test_SetBoolean(self): | 142 | config3 = git_config.GitConfig(str(rw_config_file)) |
| 141 | """Test SetBoolean behavior.""" | 143 | assert config3.GetString("foo.bar") == "valll" |
| 142 | # Set a true value. | 144 | |
| 143 | self.assertIsNone(self.config.GetBoolean("foo.bar")) | 145 | # Delete the value. |
| 144 | for val in (True, 1): | 146 | config.SetString("foo.bar", None) |
| 145 | self.config.SetBoolean("foo.bar", val) | 147 | assert config.GetString("foo.bar") is None |
| 146 | self.assertTrue(self.config.GetBoolean("foo.bar")) | 148 | config4 = git_config.GitConfig(str(rw_config_file)) |
| 147 | 149 | assert config4.GetString("foo.bar") is None | |
| 148 | # Make sure the value was actually written out. | 150 | |
| 149 | config = self.get_config() | 151 | |
| 150 | self.assertTrue(config.GetBoolean("foo.bar")) | 152 | def test_set_boolean(rw_config_file: Path) -> None: |
| 151 | self.assertEqual("true", config.GetString("foo.bar")) | 153 | """Test SetBoolean behavior.""" |
| 152 | 154 | config = git_config.GitConfig(str(rw_config_file)) | |
| 153 | # Set a false value. | 155 | |
| 154 | for val in (False, 0): | 156 | # Set a true value. |
| 155 | self.config.SetBoolean("foo.bar", val) | 157 | assert config.GetBoolean("foo.bar") is None |
| 156 | self.assertFalse(self.config.GetBoolean("foo.bar")) | 158 | for val in (True, 1): |
| 157 | 159 | config.SetBoolean("foo.bar", val) | |
| 158 | # Make sure the value was actually written out. | 160 | assert config.GetBoolean("foo.bar") is True |
| 159 | config = self.get_config() | 161 | |
| 160 | self.assertFalse(config.GetBoolean("foo.bar")) | 162 | # Make sure the value was actually written out. |
| 161 | self.assertEqual("false", config.GetString("foo.bar")) | 163 | config2 = git_config.GitConfig(str(rw_config_file)) |
| 162 | 164 | assert config2.GetBoolean("foo.bar") is True | |
| 163 | # Delete the value. | 165 | assert config2.GetString("foo.bar") == "true" |
| 164 | self.config.SetBoolean("foo.bar", None) | 166 | |
| 165 | self.assertIsNone(self.config.GetBoolean("foo.bar")) | 167 | # Set a false value. |
| 166 | config = self.get_config() | 168 | for val in (False, 0): |
| 167 | self.assertIsNone(config.GetBoolean("foo.bar")) | 169 | config.SetBoolean("foo.bar", val) |
| 168 | 170 | assert config.GetBoolean("foo.bar") is False | |
| 169 | def test_SetInt(self): | 171 | |
| 170 | """Test SetInt behavior.""" | 172 | # Make sure the value was actually written out. |
| 171 | # Set a value. | 173 | config3 = git_config.GitConfig(str(rw_config_file)) |
| 172 | self.assertIsNone(self.config.GetInt("foo.bar")) | 174 | assert config3.GetBoolean("foo.bar") is False |
| 173 | self.config.SetInt("foo.bar", 10) | 175 | assert config3.GetString("foo.bar") == "false" |
| 174 | self.assertEqual(10, self.config.GetInt("foo.bar")) | 176 | |
| 175 | 177 | # Delete the value. | |
| 176 | # Make sure the value was actually written out. | 178 | config.SetBoolean("foo.bar", None) |
| 177 | config = self.get_config() | 179 | assert config.GetBoolean("foo.bar") is None |
| 178 | self.assertEqual(10, config.GetInt("foo.bar")) | 180 | config4 = git_config.GitConfig(str(rw_config_file)) |
| 179 | self.assertEqual("10", config.GetString("foo.bar")) | 181 | assert config4.GetBoolean("foo.bar") is None |
| 180 | 182 | ||
| 181 | # Update the value. | 183 | |
| 182 | self.config.SetInt("foo.bar", 20) | 184 | def test_set_int(rw_config_file: Path) -> None: |
| 183 | self.assertEqual(20, self.config.GetInt("foo.bar")) | 185 | """Test SetInt behavior.""" |
| 184 | config = self.get_config() | 186 | config = git_config.GitConfig(str(rw_config_file)) |
| 185 | self.assertEqual(20, config.GetInt("foo.bar")) | 187 | |
| 186 | 188 | # Set a value. | |
| 187 | # Delete the value. | 189 | assert config.GetInt("foo.bar") is None |
| 188 | self.config.SetInt("foo.bar", None) | 190 | config.SetInt("foo.bar", 10) |
| 189 | self.assertIsNone(self.config.GetInt("foo.bar")) | 191 | assert config.GetInt("foo.bar") == 10 |
| 190 | config = self.get_config() | 192 | |
| 191 | self.assertIsNone(config.GetInt("foo.bar")) | 193 | # Make sure the value was actually written out. |
| 192 | 194 | config2 = git_config.GitConfig(str(rw_config_file)) | |
| 193 | def test_GetSyncAnalysisStateData(self): | 195 | assert config2.GetInt("foo.bar") == 10 |
| 194 | """Test config entries with a sync state analysis data.""" | 196 | assert config2.GetString("foo.bar") == "10" |
| 195 | superproject_logging_data = {} | 197 | |
| 196 | superproject_logging_data["test"] = False | 198 | # Update the value. |
| 197 | options = type("options", (object,), {})() | 199 | config.SetInt("foo.bar", 20) |
| 198 | options.verbose = "true" | 200 | assert config.GetInt("foo.bar") == 20 |
| 199 | options.mp_update = "false" | 201 | config3 = git_config.GitConfig(str(rw_config_file)) |
| 200 | TESTS = ( | 202 | assert config3.GetInt("foo.bar") == 20 |
| 201 | ("superproject.test", "false"), | 203 | |
| 202 | ("options.verbose", "true"), | 204 | # Delete the value. |
| 203 | ("options.mpupdate", "false"), | 205 | config.SetInt("foo.bar", None) |
| 204 | ("main.version", "1"), | 206 | assert config.GetInt("foo.bar") is None |
| 205 | ) | 207 | config4 = git_config.GitConfig(str(rw_config_file)) |
| 206 | self.config.UpdateSyncAnalysisState(options, superproject_logging_data) | 208 | assert config4.GetInt("foo.bar") is None |
| 207 | sync_data = self.config.GetSyncAnalysisStateData() | 209 | |
| 208 | for key, value in TESTS: | 210 | |
| 209 | self.assertEqual( | 211 | def test_get_sync_analysis_state_data(rw_config_file: Path) -> None: |
| 210 | sync_data[f"{git_config.SYNC_STATE_PREFIX}{key}"], value | 212 | """Test config entries with a sync state analysis data.""" |
| 211 | ) | 213 | config = git_config.GitConfig(str(rw_config_file)) |
| 212 | self.assertTrue( | 214 | superproject_logging_data: dict[str, Any] = {"test": False} |
| 213 | sync_data[f"{git_config.SYNC_STATE_PREFIX}main.synctime"] | 215 | |
| 214 | ) | 216 | class Options: |
| 217 | """Container for testing.""" | ||
| 218 | |||
| 219 | options = Options() | ||
| 220 | options.verbose = "true" | ||
| 221 | options.mp_update = "false" | ||
| 222 | |||
| 223 | TESTS = ( | ||
| 224 | ("superproject.test", "false"), | ||
| 225 | ("options.verbose", "true"), | ||
| 226 | ("options.mpupdate", "false"), | ||
| 227 | ("main.version", "1"), | ||
| 228 | ) | ||
| 229 | config.UpdateSyncAnalysisState(options, superproject_logging_data) | ||
| 230 | sync_data = config.GetSyncAnalysisStateData() | ||
| 231 | for key, value in TESTS: | ||
| 232 | assert sync_data[f"{git_config.SYNC_STATE_PREFIX}{key}"] == value | ||
| 233 | assert sync_data[f"{git_config.SYNC_STATE_PREFIX}main.synctime"] | ||
diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 76e928f7a..9d52d1849 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py | |||
| @@ -14,42 +14,47 @@ | |||
| 14 | 14 | ||
| 15 | """Unittests for the hooks.py module.""" | 15 | """Unittests for the hooks.py module.""" |
| 16 | 16 | ||
| 17 | import unittest | 17 | import pytest |
| 18 | 18 | ||
| 19 | import hooks | 19 | import hooks |
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | class RepoHookShebang(unittest.TestCase): | 22 | @pytest.mark.parametrize( |
| 23 | """Check shebang parsing in RepoHook.""" | 23 | "data", |
| 24 | 24 | ( | |
| 25 | def test_no_shebang(self): | 25 | "", |
| 26 | """Lines w/out shebangs should be rejected.""" | 26 | "#\n# foo\n", |
| 27 | DATA = ("", "#\n# foo\n", "# Bad shebang in script\n#!/foo\n") | 27 | "# Bad shebang in script\n#!/foo\n", |
| 28 | for data in DATA: | 28 | ), |
| 29 | self.assertIsNone(hooks.RepoHook._ExtractInterpFromShebang(data)) | 29 | ) |
| 30 | 30 | def test_no_shebang(data: str) -> None: | |
| 31 | def test_direct_interp(self): | 31 | """Lines w/out shebangs should be rejected.""" |
| 32 | """Lines whose shebang points directly to the interpreter.""" | 32 | assert hooks.RepoHook._ExtractInterpFromShebang(data) is None |
| 33 | DATA = ( | 33 | |
| 34 | ("#!/foo", "/foo"), | 34 | |
| 35 | ("#! /foo", "/foo"), | 35 | @pytest.mark.parametrize( |
| 36 | ("#!/bin/foo ", "/bin/foo"), | 36 | "shebang, interp", |
| 37 | ("#! /usr/foo ", "/usr/foo"), | 37 | ( |
| 38 | ("#! /usr/foo -args", "/usr/foo"), | 38 | ("#!/foo", "/foo"), |
| 39 | ) | 39 | ("#! /foo", "/foo"), |
| 40 | for shebang, interp in DATA: | 40 | ("#!/bin/foo ", "/bin/foo"), |
| 41 | self.assertEqual( | 41 | ("#! /usr/foo ", "/usr/foo"), |
| 42 | hooks.RepoHook._ExtractInterpFromShebang(shebang), interp | 42 | ("#! /usr/foo -args", "/usr/foo"), |
| 43 | ) | 43 | ), |
| 44 | 44 | ) | |
| 45 | def test_env_interp(self): | 45 | def test_direct_interp(shebang: str, interp: str) -> None: |
| 46 | """Lines whose shebang launches through `env`.""" | 46 | """Lines whose shebang points directly to the interpreter.""" |
| 47 | DATA = ( | 47 | assert hooks.RepoHook._ExtractInterpFromShebang(shebang) == interp |
| 48 | ("#!/usr/bin/env foo", "foo"), | 48 | |
| 49 | ("#!/bin/env foo", "foo"), | 49 | |
| 50 | ("#! /bin/env /bin/foo ", "/bin/foo"), | 50 | @pytest.mark.parametrize( |
| 51 | ) | 51 | "shebang, interp", |
| 52 | for shebang, interp in DATA: | 52 | ( |
| 53 | self.assertEqual( | 53 | ("#!/usr/bin/env foo", "foo"), |
| 54 | hooks.RepoHook._ExtractInterpFromShebang(shebang), interp | 54 | ("#!/bin/env foo", "foo"), |
| 55 | ) | 55 | ("#! /bin/env /bin/foo ", "/bin/foo"), |
| 56 | ), | ||
| 57 | ) | ||
| 58 | def test_env_interp(shebang: str, interp: str) -> None: | ||
| 59 | """Lines whose shebang launches through `env`.""" | ||
| 60 | assert hooks.RepoHook._ExtractInterpFromShebang(shebang) == interp | ||
diff --git a/tests/test_platform_utils.py b/tests/test_platform_utils.py index aab8a52ab..e89965391 100644 --- a/tests/test_platform_utils.py +++ b/tests/test_platform_utils.py | |||
| @@ -14,39 +14,35 @@ | |||
| 14 | 14 | ||
| 15 | """Unittests for the platform_utils.py module.""" | 15 | """Unittests for the platform_utils.py module.""" |
| 16 | 16 | ||
| 17 | import os | 17 | from pathlib import Path |
| 18 | import tempfile | ||
| 19 | import unittest | ||
| 20 | 18 | ||
| 21 | import platform_utils | 19 | import pytest |
| 22 | 20 | ||
| 21 | import platform_utils | ||
| 23 | 22 | ||
| 24 | class RemoveTests(unittest.TestCase): | ||
| 25 | """Check remove() helper.""" | ||
| 26 | 23 | ||
| 27 | def testMissingOk(self): | 24 | def test_remove_missing_ok(tmp_path: Path) -> None: |
| 28 | """Check missing_ok handling.""" | 25 | """Check missing_ok handling.""" |
| 29 | with tempfile.TemporaryDirectory() as tmpdir: | 26 | path = tmp_path / "test" |
| 30 | path = os.path.join(tmpdir, "test") | ||
| 31 | 27 | ||
| 32 | # Should not fail. | 28 | # Should not fail. |
| 33 | platform_utils.remove(path, missing_ok=True) | 29 | platform_utils.remove(path, missing_ok=True) |
| 34 | 30 | ||
| 35 | # Should fail. | 31 | # Should fail. |
| 36 | self.assertRaises(OSError, platform_utils.remove, path) | 32 | with pytest.raises(OSError): |
| 37 | self.assertRaises( | 33 | platform_utils.remove(path) |
| 38 | OSError, platform_utils.remove, path, missing_ok=False | 34 | with pytest.raises(OSError): |
| 39 | ) | 35 | platform_utils.remove(path, missing_ok=False) |
| 40 | 36 | ||
| 41 | # Should not fail if it exists. | 37 | # Should not fail if it exists. |
| 42 | open(path, "w").close() | 38 | path.touch() |
| 43 | platform_utils.remove(path, missing_ok=True) | 39 | platform_utils.remove(path, missing_ok=True) |
| 44 | self.assertFalse(os.path.exists(path)) | 40 | assert not path.exists() |
| 45 | 41 | ||
| 46 | open(path, "w").close() | 42 | path.touch() |
| 47 | platform_utils.remove(path) | 43 | platform_utils.remove(path) |
| 48 | self.assertFalse(os.path.exists(path)) | 44 | assert not path.exists() |
| 49 | 45 | ||
| 50 | open(path, "w").close() | 46 | path.touch() |
| 51 | platform_utils.remove(path, missing_ok=False) | 47 | platform_utils.remove(path, missing_ok=False) |
| 52 | self.assertFalse(os.path.exists(path)) | 48 | assert not path.exists() |
diff --git a/tests/test_repo_logging.py b/tests/test_repo_logging.py index e072039ed..c5bba6b8e 100644 --- a/tests/test_repo_logging.py +++ b/tests/test_repo_logging.py | |||
| @@ -12,90 +12,96 @@ | |||
| 12 | # See the License for the specific language governing permissions and | 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. | 13 | # limitations under the License. |
| 14 | 14 | ||
| 15 | """Unit test for repo_logging module.""" | 15 | """Unittests for the repo_logging.py module.""" |
| 16 | 16 | ||
| 17 | import contextlib | 17 | import contextlib |
| 18 | import io | 18 | import io |
| 19 | import logging | 19 | import logging |
| 20 | import unittest | 20 | import re |
| 21 | from unittest import mock | 21 | from unittest import mock |
| 22 | 22 | ||
| 23 | import pytest | ||
| 24 | |||
| 23 | from color import SetDefaultColoring | 25 | from color import SetDefaultColoring |
| 24 | from error import RepoExitError | 26 | from error import RepoExitError |
| 25 | from repo_logging import RepoLogger | 27 | from repo_logging import RepoLogger |
| 26 | 28 | ||
| 27 | 29 | ||
| 28 | class TestRepoLogger(unittest.TestCase): | 30 | @mock.patch.object(RepoLogger, "error") |
| 29 | @mock.patch.object(RepoLogger, "error") | 31 | def test_log_aggregated_errors_logs_aggregated_errors(mock_error) -> None: |
| 30 | def test_log_aggregated_errors_logs_aggregated_errors(self, mock_error): | 32 | """Test if log_aggregated_errors logs a list of aggregated errors.""" |
| 31 | """Test if log_aggregated_errors logs a list of aggregated errors.""" | 33 | logger = RepoLogger(__name__) |
| 32 | logger = RepoLogger(__name__) | 34 | logger.log_aggregated_errors( |
| 33 | logger.log_aggregated_errors( | 35 | RepoExitError( |
| 34 | RepoExitError( | 36 | aggregate_errors=[ |
| 35 | aggregate_errors=[ | 37 | Exception("foo"), |
| 36 | Exception("foo"), | 38 | Exception("bar"), |
| 37 | Exception("bar"), | 39 | Exception("baz"), |
| 38 | Exception("baz"), | 40 | Exception("hello"), |
| 39 | Exception("hello"), | 41 | Exception("world"), |
| 40 | Exception("world"), | 42 | Exception("test"), |
| 41 | Exception("test"), | ||
| 42 | ] | ||
| 43 | ) | ||
| 44 | ) | ||
| 45 | |||
| 46 | mock_error.assert_has_calls( | ||
| 47 | [ | ||
| 48 | mock.call("=" * 80), | ||
| 49 | mock.call( | ||
| 50 | "Repo command failed due to the following `%s` errors:", | ||
| 51 | "RepoExitError", | ||
| 52 | ), | ||
| 53 | mock.call("foo\nbar\nbaz\nhello\nworld"), | ||
| 54 | mock.call("+%d additional errors...", 1), | ||
| 55 | ] | 43 | ] |
| 56 | ) | 44 | ) |
| 45 | ) | ||
| 57 | 46 | ||
| 58 | @mock.patch.object(RepoLogger, "error") | 47 | mock_error.assert_has_calls( |
| 59 | def test_log_aggregated_errors_logs_single_error(self, mock_error): | 48 | [ |
| 60 | """Test if log_aggregated_errors logs empty aggregated_errors.""" | 49 | mock.call("=" * 80), |
| 61 | logger = RepoLogger(__name__) | 50 | mock.call( |
| 62 | logger.log_aggregated_errors(RepoExitError()) | 51 | "Repo command failed due to the following `%s` errors:", |
| 52 | "RepoExitError", | ||
| 53 | ), | ||
| 54 | mock.call("foo\nbar\nbaz\nhello\nworld"), | ||
| 55 | mock.call("+%d additional errors...", 1), | ||
| 56 | ] | ||
| 57 | ) | ||
| 63 | 58 | ||
| 64 | mock_error.assert_has_calls( | ||
| 65 | [ | ||
| 66 | mock.call("=" * 80), | ||
| 67 | mock.call("Repo command failed: %s", "RepoExitError"), | ||
| 68 | ] | ||
| 69 | ) | ||
| 70 | 59 | ||
| 71 | def test_log_with_format_string(self): | 60 | @mock.patch.object(RepoLogger, "error") |
| 72 | """Test different log levels with format strings.""" | 61 | def test_log_aggregated_errors_logs_single_error(mock_error) -> None: |
| 73 | 62 | """Test if log_aggregated_errors logs empty aggregated_errors.""" | |
| 74 | # Set color output to "always" for consistent test results. | 63 | logger = RepoLogger(__name__) |
| 75 | # This ensures the logger's behavior is uniform across different | 64 | logger.log_aggregated_errors(RepoExitError()) |
| 76 | # environments and git configurations. | 65 | |
| 77 | SetDefaultColoring("always") | 66 | mock_error.assert_has_calls( |
| 78 | 67 | [ | |
| 79 | # Regex pattern to match optional ANSI color codes. | 68 | mock.call("=" * 80), |
| 80 | # \033 - Escape character | 69 | mock.call("Repo command failed: %s", "RepoExitError"), |
| 81 | # \[ - Opening square bracket | 70 | ] |
| 82 | # [0-9;]* - Zero or more digits or semicolons | 71 | ) |
| 83 | # m - Ending 'm' character | 72 | |
| 84 | # ? - Makes the entire group optional | 73 | |
| 85 | opt_color = r"(\033\[[0-9;]*m)?" | 74 | @pytest.mark.parametrize( |
| 86 | 75 | "level", | |
| 87 | for level in (logging.INFO, logging.WARN, logging.ERROR): | 76 | ( |
| 88 | name = logging.getLevelName(level) | 77 | logging.INFO, |
| 89 | 78 | logging.WARN, | |
| 90 | with self.subTest(level=level, name=name): | 79 | logging.ERROR, |
| 91 | output = io.StringIO() | 80 | ), |
| 92 | 81 | ) | |
| 93 | with contextlib.redirect_stderr(output): | 82 | def test_log_with_format_string(level: int) -> None: |
| 94 | logger = RepoLogger(__name__) | 83 | """Test different log levels with format strings.""" |
| 95 | logger.log(level, "%s", "100% pass") | 84 | name = logging.getLevelName(level) |
| 96 | 85 | ||
| 97 | self.assertRegex( | 86 | # Set color output to "always" for consistent test results. |
| 98 | output.getvalue().strip(), | 87 | # This ensures the logger's behavior is uniform across different |
| 99 | f"^{opt_color}100% pass{opt_color}$", | 88 | # environments and git configurations. |
| 100 | f"failed for level {name}", | 89 | SetDefaultColoring("always") |
| 101 | ) | 90 | |
| 91 | # Regex pattern to match optional ANSI color codes. | ||
| 92 | # \033 - Escape character | ||
| 93 | # \[ - Opening square bracket | ||
| 94 | # [0-9;]* - Zero or more digits or semicolons | ||
| 95 | # m - Ending 'm' character | ||
| 96 | # ? - Makes the entire group optional | ||
| 97 | opt_color = r"(\033\[[0-9;]*m)?" | ||
| 98 | |||
| 99 | output = io.StringIO() | ||
| 100 | |||
| 101 | with contextlib.redirect_stderr(output): | ||
| 102 | logger = RepoLogger(__name__) | ||
| 103 | logger.log(level, "%s", "100% pass") | ||
| 104 | |||
| 105 | assert re.search( | ||
| 106 | f"^{opt_color}100% pass{opt_color}$", output.getvalue().strip() | ||
| 107 | ), f"failed for level {name}" | ||
diff --git a/tests/test_repo_trace.py b/tests/test_repo_trace.py index 2ea341025..3ec540b25 100644 --- a/tests/test_repo_trace.py +++ b/tests/test_repo_trace.py | |||
| @@ -15,46 +15,37 @@ | |||
| 15 | """Unittests for the repo_trace.py module.""" | 15 | """Unittests for the repo_trace.py module.""" |
| 16 | 16 | ||
| 17 | import os | 17 | import os |
| 18 | import unittest | 18 | |
| 19 | from unittest import mock | 19 | import pytest |
| 20 | 20 | ||
| 21 | import repo_trace | 21 | import repo_trace |
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | class TraceTests(unittest.TestCase): | 24 | def test_trace_max_size_enforced(monkeypatch: pytest.MonkeyPatch) -> None: |
| 25 | """Check Trace behavior.""" | 25 | """Check Trace behavior.""" |
| 26 | 26 | content = "git chicken" | |
| 27 | def testTrace_MaxSizeEnforced(self): | 27 | |
| 28 | content = "git chicken" | 28 | with repo_trace.Trace(content, first_trace=True): |
| 29 | 29 | pass | |
| 30 | with repo_trace.Trace(content, first_trace=True): | 30 | first_trace_size = os.path.getsize(repo_trace._TRACE_FILE) |
| 31 | pass | 31 | |
| 32 | first_trace_size = os.path.getsize(repo_trace._TRACE_FILE) | 32 | with repo_trace.Trace(content): |
| 33 | 33 | pass | |
| 34 | with repo_trace.Trace(content): | 34 | assert os.path.getsize(repo_trace._TRACE_FILE) > first_trace_size |
| 35 | pass | 35 | |
| 36 | self.assertGreater( | 36 | # Check we clear everything if the last chunk is larger than _MAX_SIZE. |
| 37 | os.path.getsize(repo_trace._TRACE_FILE), first_trace_size | 37 | monkeypatch.setattr(repo_trace, "_MAX_SIZE", 0) |
| 38 | ) | 38 | with repo_trace.Trace(content, first_trace=True): |
| 39 | 39 | pass | |
| 40 | # Check we clear everything is the last chunk is larger than _MAX_SIZE. | 40 | assert os.path.getsize(repo_trace._TRACE_FILE) == first_trace_size |
| 41 | with mock.patch("repo_trace._MAX_SIZE", 0): | 41 | |
| 42 | with repo_trace.Trace(content, first_trace=True): | 42 | # Check we only clear the chunks we need to. |
| 43 | pass | 43 | new_max = (first_trace_size + 1) / (1024 * 1024) |
| 44 | self.assertEqual( | 44 | monkeypatch.setattr(repo_trace, "_MAX_SIZE", new_max) |
| 45 | first_trace_size, os.path.getsize(repo_trace._TRACE_FILE) | 45 | with repo_trace.Trace(content, first_trace=True): |
| 46 | ) | 46 | pass |
| 47 | 47 | assert os.path.getsize(repo_trace._TRACE_FILE) == first_trace_size * 2 | |
| 48 | # Check we only clear the chunks we need to. | 48 | |
| 49 | repo_trace._MAX_SIZE = (first_trace_size + 1) / (1024 * 1024) | 49 | with repo_trace.Trace(content, first_trace=True): |
| 50 | with repo_trace.Trace(content, first_trace=True): | 50 | pass |
| 51 | pass | 51 | assert os.path.getsize(repo_trace._TRACE_FILE) == first_trace_size * 2 |
| 52 | self.assertEqual( | ||
| 53 | first_trace_size * 2, os.path.getsize(repo_trace._TRACE_FILE) | ||
| 54 | ) | ||
| 55 | |||
| 56 | with repo_trace.Trace(content, first_trace=True): | ||
| 57 | pass | ||
| 58 | self.assertEqual( | ||
| 59 | first_trace_size * 2, os.path.getsize(repo_trace._TRACE_FILE) | ||
| 60 | ) | ||
diff --git a/tests/test_ssh.py b/tests/test_ssh.py index ce65fac45..6bb2c2f4e 100644 --- a/tests/test_ssh.py +++ b/tests/test_ssh.py | |||
| @@ -16,72 +16,84 @@ | |||
| 16 | 16 | ||
| 17 | import multiprocessing | 17 | import multiprocessing |
| 18 | import subprocess | 18 | import subprocess |
| 19 | import unittest | 19 | from typing import Tuple |
| 20 | from unittest import mock | 20 | from unittest import mock |
| 21 | 21 | ||
| 22 | import pytest | ||
| 23 | |||
| 22 | import ssh | 24 | import ssh |
| 23 | 25 | ||
| 24 | 26 | ||
| 25 | class SshTests(unittest.TestCase): | 27 | @pytest.fixture(autouse=True) |
| 26 | """Tests the ssh functions.""" | 28 | def clear_ssh_version_cache() -> None: |
| 29 | """Clear the ssh version cache before each test.""" | ||
| 30 | ssh.version.cache_clear() | ||
| 27 | 31 | ||
| 28 | def setUp(self) -> None: | ||
| 29 | super().setUp() | ||
| 30 | ssh.version.cache_clear() | ||
| 31 | 32 | ||
| 32 | def test_parse_ssh_version(self): | 33 | @pytest.mark.parametrize( |
| 33 | """Check _parse_ssh_version() handling.""" | 34 | "input_str, expected", |
| 34 | ver = ssh._parse_ssh_version("Unknown\n") | 35 | ( |
| 35 | self.assertEqual(ver, ()) | 36 | ("Unknown\n", ()), |
| 36 | ver = ssh._parse_ssh_version("OpenSSH_1.0\n") | 37 | ("OpenSSH_1.0\n", (1, 0)), |
| 37 | self.assertEqual(ver, (1, 0)) | 38 | ( |
| 38 | ver = ssh._parse_ssh_version( | 39 | "OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n", |
| 39 | "OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n" | 40 | (6, 6, 1), |
| 40 | ) | 41 | ), |
| 41 | self.assertEqual(ver, (6, 6, 1)) | 42 | ( |
| 42 | ver = ssh._parse_ssh_version( | 43 | "OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n", |
| 43 | "OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n" | 44 | (7, 6), |
| 44 | ) | 45 | ), |
| 45 | self.assertEqual(ver, (7, 6)) | 46 | ("OpenSSH_9.0p1, LibreSSL 3.3.6\n", (9, 0)), |
| 46 | ver = ssh._parse_ssh_version("OpenSSH_9.0p1, LibreSSL 3.3.6\n") | 47 | ), |
| 47 | self.assertEqual(ver, (9, 0)) | 48 | ) |
| 48 | 49 | def test_parse_ssh_version(input_str: str, expected: Tuple[int, ...]) -> None: | |
| 49 | def test_version(self): | 50 | """Check _parse_ssh_version() handling.""" |
| 50 | """Check version() handling.""" | 51 | assert ssh._parse_ssh_version(input_str) == expected |
| 51 | with mock.patch("ssh._run_ssh_version", return_value="OpenSSH_1.2\n"): | 52 | |
| 52 | self.assertEqual(ssh.version(), (1, 2)) | 53 | |
| 53 | 54 | def test_version() -> None: | |
| 54 | def test_context_manager_empty(self): | 55 | """Check version() handling.""" |
| 55 | """Verify context manager with no clients works correctly.""" | 56 | with mock.patch("ssh._run_ssh_version", return_value="OpenSSH_1.2\n"): |
| 56 | with multiprocessing.Manager() as manager: | 57 | assert ssh.version() == (1, 2) |
| 57 | with ssh.ProxyManager(manager): | 58 | |
| 58 | pass | 59 | |
| 59 | 60 | def test_context_manager_empty() -> None: | |
| 60 | def test_context_manager_child_cleanup(self): | 61 | """Verify context manager with no clients works correctly.""" |
| 61 | """Verify orphaned clients & masters get cleaned up.""" | 62 | with multiprocessing.Manager() as manager: |
| 62 | with multiprocessing.Manager() as manager: | 63 | with ssh.ProxyManager(manager): |
| 63 | with mock.patch("ssh.version", return_value=(1, 2)): | 64 | pass |
| 64 | with ssh.ProxyManager(manager) as ssh_proxy: | 65 | |
| 65 | client = subprocess.Popen(["sleep", "964853320"]) | 66 | |
| 66 | ssh_proxy.add_client(client) | 67 | def test_context_manager_child_cleanup() -> None: |
| 67 | master = subprocess.Popen(["sleep", "964853321"]) | 68 | """Verify orphaned clients & masters get cleaned up.""" |
| 68 | ssh_proxy.add_master(master) | 69 | with multiprocessing.Manager() as manager: |
| 69 | # If the process still exists, these will throw timeout errors. | 70 | with mock.patch("ssh.version", return_value=(1, 2)): |
| 70 | client.wait(0) | 71 | with ssh.ProxyManager(manager) as ssh_proxy: |
| 71 | master.wait(0) | 72 | client = subprocess.Popen(["sleep", "964853320"]) |
| 72 | 73 | ssh_proxy.add_client(client) | |
| 73 | def test_ssh_sock(self): | 74 | master = subprocess.Popen(["sleep", "964853321"]) |
| 74 | """Check sock() function.""" | 75 | ssh_proxy.add_master(master) |
| 75 | manager = multiprocessing.Manager() | 76 | # If the process still exists, these will throw timeout errors. |
| 77 | client.wait(0) | ||
| 78 | master.wait(0) | ||
| 79 | |||
| 80 | |||
| 81 | def test_ssh_sock(monkeypatch: pytest.MonkeyPatch) -> None: | ||
| 82 | """Check sock() function.""" | ||
| 83 | with multiprocessing.Manager() as manager: | ||
| 76 | proxy = ssh.ProxyManager(manager) | 84 | proxy = ssh.ProxyManager(manager) |
| 77 | with mock.patch("tempfile.mkdtemp", return_value="/tmp/foo"): | 85 | monkeypatch.setattr( |
| 78 | # Old ssh version uses port. | 86 | "tempfile.mkdtemp", lambda *args, **kwargs: "/tmp/foo" |
| 79 | with mock.patch("ssh.version", return_value=(6, 6)): | 87 | ) |
| 80 | with proxy as ssh_proxy: | 88 | |
| 81 | self.assertTrue(ssh_proxy.sock().endswith("%p")) | 89 | # Old ssh version uses port. |
| 82 | 90 | with mock.patch("ssh.version", return_value=(6, 6)): | |
| 83 | proxy._sock_path = None | 91 | with proxy as ssh_proxy: |
| 84 | # New ssh version uses hash. | 92 | assert ssh_proxy.sock().endswith("%p") |
| 85 | with mock.patch("ssh.version", return_value=(6, 7)): | 93 | |
| 86 | with proxy as ssh_proxy: | 94 | proxy._sock_path = None |
| 87 | self.assertTrue(ssh_proxy.sock().endswith("%C")) | 95 | # New ssh version uses hash. |
| 96 | with mock.patch("ssh.version", return_value=(6, 7)): | ||
| 97 | with proxy as ssh_proxy: | ||
| 98 | assert ssh_proxy.sock().endswith("%C") | ||
| 99 | proxy._sock_path = None | ||
diff --git a/tests/test_subcmds.py b/tests/test_subcmds.py index 3382d822d..12bbb2700 100644 --- a/tests/test_subcmds.py +++ b/tests/test_subcmds.py | |||
| @@ -15,170 +15,164 @@ | |||
| 15 | """Unittests for the subcmds module (mostly __init__.py than subcommands).""" | 15 | """Unittests for the subcmds module (mostly __init__.py than subcommands).""" |
| 16 | 16 | ||
| 17 | import optparse | 17 | import optparse |
| 18 | import unittest | 18 | from typing import Type |
| 19 | |||
| 20 | import subcmds | ||
| 21 | 19 | ||
| 20 | import pytest | ||
| 22 | 21 | ||
| 23 | class AllCommands(unittest.TestCase): | 22 | from command import Command |
| 24 | """Check registered all_commands.""" | 23 | import subcmds |
| 25 | 24 | ||
| 26 | def test_required_basic(self): | ||
| 27 | """Basic checking of registered commands.""" | ||
| 28 | # NB: We don't test all subcommands as we want to avoid "change | ||
| 29 | # detection" tests, so we just look for the most common/important ones | ||
| 30 | # here that are unlikely to ever change. | ||
| 31 | for cmd in {"cherry-pick", "help", "init", "start", "sync", "upload"}: | ||
| 32 | self.assertIn(cmd, subcmds.all_commands) | ||
| 33 | 25 | ||
| 34 | def test_naming(self): | 26 | # NB: We don't test all subcommands as we want to avoid "change detection" |
| 35 | """Verify we don't add things that we shouldn't.""" | 27 | # tests, so we just look for the most common/important ones here that are |
| 36 | for cmd in subcmds.all_commands: | 28 | # unlikely to ever change. |
| 37 | # Reject filename suffixes like "help.py". | 29 | @pytest.mark.parametrize( |
| 38 | self.assertNotIn(".", cmd) | 30 | "cmd", ("cherry-pick", "help", "init", "start", "sync", "upload") |
| 31 | ) | ||
| 32 | def test_required_basic(cmd: str) -> None: | ||
| 33 | """Basic checking of registered commands.""" | ||
| 34 | assert cmd in subcmds.all_commands | ||
| 35 | |||
| 36 | |||
| 37 | @pytest.mark.parametrize("name", subcmds.all_commands.keys()) | ||
| 38 | def test_naming(name: str) -> None: | ||
| 39 | """Verify we don't add things that we shouldn't.""" | ||
| 40 | # Reject filename suffixes like "help.py". | ||
| 41 | assert "." not in name | ||
| 42 | |||
| 43 | # Make sure all '_' were converted to '-'. | ||
| 44 | assert "_" not in name | ||
| 45 | |||
| 46 | # Reject internal python paths like "__init__". | ||
| 47 | assert not name.startswith("__") | ||
| 48 | |||
| 49 | |||
| 50 | @pytest.mark.parametrize("name, cls", subcmds.all_commands.items()) | ||
| 51 | def test_help_desc_style(name: str, cls: Type[Command]) -> None: | ||
| 52 | """Force some consistency in option descriptions. | ||
| 53 | |||
| 54 | Python's optparse & argparse has a few default options like --help. | ||
| 55 | Their option description text uses lowercase sentence fragments, so | ||
| 56 | enforce our options follow the same style so UI is consistent. | ||
| 57 | |||
| 58 | We enforce: | ||
| 59 | * Text starts with lowercase. | ||
| 60 | * Text doesn't end with period. | ||
| 61 | """ | ||
| 62 | cmd = cls() | ||
| 63 | parser = cmd.OptionParser | ||
| 64 | for option in parser.option_list: | ||
| 65 | if option.help == optparse.SUPPRESS_HELP or not option.help: | ||
| 66 | continue | ||
| 67 | |||
| 68 | c = option.help[0] | ||
| 69 | assert c.lower() == c, ( | ||
| 70 | f"subcmds/{name}.py: {option.get_opt_string()}: " | ||
| 71 | f'help text should start with lowercase: "{option.help}"' | ||
| 72 | ) | ||
| 73 | |||
| 74 | assert option.help[-1] != ".", ( | ||
| 75 | f"subcmds/{name}.py: {option.get_opt_string()}: " | ||
| 76 | f'help text should not end in a period: "{option.help}"' | ||
| 77 | ) | ||
| 78 | |||
| 79 | |||
| 80 | @pytest.mark.parametrize("name, cls", subcmds.all_commands.items()) | ||
| 81 | def test_cli_option_style(name: str, cls: Type[Command]) -> None: | ||
| 82 | """Force some consistency in option flags.""" | ||
| 83 | cmd = cls() | ||
| 84 | parser = cmd.OptionParser | ||
| 85 | for option in parser.option_list: | ||
| 86 | for opt in option._long_opts: | ||
| 87 | assert "_" not in opt, ( | ||
| 88 | f"subcmds/{name}.py: {opt}: only use dashes in " | ||
| 89 | "options, not underscores" | ||
| 90 | ) | ||
| 39 | 91 | ||
| 40 | # Make sure all '_' were converted to '-'. | ||
| 41 | self.assertNotIn("_", cmd) | ||
| 42 | 92 | ||
| 43 | # Reject internal python paths like "__init__". | 93 | def test_cli_option_dest() -> None: |
| 44 | self.assertFalse(cmd.startswith("__")) | 94 | """Block redundant dest= arguments.""" |
| 95 | bad_opts: list[tuple[str, str]] = [] | ||
| 45 | 96 | ||
| 46 | def test_help_desc_style(self): | 97 | def _check_dest(opt: optparse.Option) -> None: |
| 47 | """Force some consistency in option descriptions. | 98 | """Check the dest= setting.""" |
| 99 | # If the destination is not set, nothing to check. | ||
| 100 | # If long options are not set, then there's no implicit destination. | ||
| 101 | # If callback is used, then a destination might be needed because | ||
| 102 | # optparse cannot assume a value is always stored. | ||
| 103 | if opt.dest is None or not opt._long_opts or opt.callback: | ||
| 104 | return | ||
| 48 | 105 | ||
| 49 | Python's optparse & argparse has a few default options like --help. | 106 | long = opt._long_opts[0] |
| 50 | Their option description text uses lowercase sentence fragments, so | 107 | assert long.startswith("--") |
| 51 | enforce our options follow the same style so UI is consistent. | 108 | # This matches optparse's behavior. |
| 109 | implicit_dest = long[2:].replace("-", "_") | ||
| 110 | if implicit_dest == opt.dest: | ||
| 111 | bad_opts.append((str(opt), opt.dest)) | ||
| 52 | 112 | ||
| 53 | We enforce: | 113 | # Hook the option check list. |
| 54 | * Text starts with lowercase. | 114 | optparse.Option.CHECK_METHODS.insert(0, _check_dest) |
| 55 | * Text doesn't end with period. | ||
| 56 | """ | ||
| 57 | for name, cls in subcmds.all_commands.items(): | ||
| 58 | cmd = cls() | ||
| 59 | parser = cmd.OptionParser | ||
| 60 | for option in parser.option_list: | ||
| 61 | if option.help == optparse.SUPPRESS_HELP: | ||
| 62 | continue | ||
| 63 | |||
| 64 | c = option.help[0] | ||
| 65 | self.assertEqual( | ||
| 66 | c.lower(), | ||
| 67 | c, | ||
| 68 | msg=f"subcmds/{name}.py: {option.get_opt_string()}: " | ||
| 69 | f'help text should start with lowercase: "{option.help}"', | ||
| 70 | ) | ||
| 71 | |||
| 72 | self.assertNotEqual( | ||
| 73 | option.help[-1], | ||
| 74 | ".", | ||
| 75 | msg=f"subcmds/{name}.py: {option.get_opt_string()}: " | ||
| 76 | f'help text should not end in a period: "{option.help}"', | ||
| 77 | ) | ||
| 78 | |||
| 79 | def test_cli_option_style(self): | ||
| 80 | """Force some consistency in option flags.""" | ||
| 81 | for name, cls in subcmds.all_commands.items(): | ||
| 82 | cmd = cls() | ||
| 83 | parser = cmd.OptionParser | ||
| 84 | for option in parser.option_list: | ||
| 85 | for opt in option._long_opts: | ||
| 86 | self.assertNotIn( | ||
| 87 | "_", | ||
| 88 | opt, | ||
| 89 | msg=f"subcmds/{name}.py: {opt}: only use dashes in " | ||
| 90 | "options, not underscores", | ||
| 91 | ) | ||
| 92 | |||
| 93 | def test_cli_option_dest(self): | ||
| 94 | """Block redundant dest= arguments.""" | ||
| 95 | |||
| 96 | def _check_dest(opt): | ||
| 97 | """Check the dest= setting.""" | ||
| 98 | # If the destination is not set, nothing to check. | ||
| 99 | # If long options are not set, then there's no implicit destination. | ||
| 100 | # If callback is used, then a destination might be needed because | ||
| 101 | # optparse cannot assume a value is always stored. | ||
| 102 | if opt.dest is None or not opt._long_opts or opt.callback: | ||
| 103 | return | ||
| 104 | |||
| 105 | long = opt._long_opts[0] | ||
| 106 | assert long.startswith("--") | ||
| 107 | # This matches optparse's behavior. | ||
| 108 | implicit_dest = long[2:].replace("-", "_") | ||
| 109 | if implicit_dest == opt.dest: | ||
| 110 | bad_opts.append((str(opt), opt.dest)) | ||
| 111 | |||
| 112 | # Hook the option check list. | ||
| 113 | optparse.Option.CHECK_METHODS.insert(0, _check_dest) | ||
| 114 | 115 | ||
| 116 | try: | ||
| 115 | # Gather all the bad options up front so people can see all bad options | 117 | # Gather all the bad options up front so people can see all bad options |
| 116 | # instead of failing at the first one. | 118 | # instead of failing at the first one. |
| 117 | all_bad_opts = {} | 119 | all_bad_opts: dict[str, list[tuple[str, str]]] = {} |
| 118 | for name, cls in subcmds.all_commands.items(): | 120 | for name, cls in subcmds.all_commands.items(): |
| 119 | bad_opts = all_bad_opts[name] = [] | 121 | bad_opts = [] |
| 120 | cmd = cls() | 122 | cmd = cls() |
| 121 | # Trigger construction of parser. | 123 | # Trigger construction of parser. |
| 122 | cmd.OptionParser | 124 | _ = cmd.OptionParser |
| 125 | all_bad_opts[name] = bad_opts | ||
| 123 | 126 | ||
| 124 | errmsg = None | 127 | errmsg = "" |
| 125 | for name, bad_opts in sorted(all_bad_opts.items()): | 128 | for name, bad_opts_list in sorted(all_bad_opts.items()): |
| 126 | if bad_opts: | 129 | if bad_opts_list: |
| 127 | if not errmsg: | 130 | if not errmsg: |
| 128 | errmsg = "Omit redundant dest= when defining options.\n" | 131 | errmsg = "Omit redundant dest= when defining options.\n" |
| 129 | errmsg += f"\nSubcommand {name} (subcmds/{name}.py):\n" | 132 | errmsg += f"\nSubcommand {name} (subcmds/{name}.py):\n" |
| 130 | errmsg += "".join( | 133 | errmsg += "".join( |
| 131 | f" {opt}: dest='{dest}'\n" for opt, dest in bad_opts | 134 | f" {opt}: dest='{dest}'\n" for opt, dest in bad_opts_list |
| 132 | ) | 135 | ) |
| 133 | if errmsg: | 136 | if errmsg: |
| 134 | self.fail(errmsg) | 137 | pytest.fail(errmsg) |
| 135 | 138 | finally: | |
| 136 | # Make sure we aren't popping the wrong stuff. | 139 | # Make sure we aren't popping the wrong stuff. |
| 137 | assert optparse.Option.CHECK_METHODS.pop(0) is _check_dest | 140 | assert optparse.Option.CHECK_METHODS.pop(0) is _check_dest |
| 138 | 141 | ||
| 139 | def test_common_validate_options(self): | ||
| 140 | """Verify CommonValidateOptions sets up expected fields.""" | ||
| 141 | for name, cls in subcmds.all_commands.items(): | ||
| 142 | cmd = cls() | ||
| 143 | opts, args = cmd.OptionParser.parse_args([]) | ||
| 144 | 142 | ||
| 145 | # Verify the fields don't exist yet. | 143 | @pytest.mark.parametrize("name, cls", subcmds.all_commands.items()) |
| 146 | self.assertFalse( | 144 | def test_common_validate_options(name: str, cls: Type[Command]) -> None: |
| 147 | hasattr(opts, "verbose"), | 145 | """Verify CommonValidateOptions sets up expected fields.""" |
| 148 | msg=f"{name}: has verbose before validation", | 146 | cmd = cls() |
| 149 | ) | 147 | opts, args = cmd.OptionParser.parse_args([]) |
| 150 | self.assertFalse( | ||
| 151 | hasattr(opts, "quiet"), | ||
| 152 | msg=f"{name}: has quiet before validation", | ||
| 153 | ) | ||
| 154 | 148 | ||
| 155 | cmd.CommonValidateOptions(opts, args) | 149 | # Verify the fields don't exist yet. |
| 150 | assert not hasattr( | ||
| 151 | opts, "verbose" | ||
| 152 | ), f"{name}: has verbose before validation" | ||
| 153 | assert not hasattr(opts, "quiet"), f"{name}: has quiet before validation" | ||
| 154 | |||
| 155 | cmd.CommonValidateOptions(opts, args) | ||
| 156 | |||
| 157 | # Verify the fields exist now. | ||
| 158 | assert hasattr(opts, "verbose"), f"{name}: missing verbose after validation" | ||
| 159 | assert hasattr(opts, "quiet"), f"{name}: missing quiet after validation" | ||
| 160 | assert hasattr( | ||
| 161 | opts, "outer_manifest" | ||
| 162 | ), f"{name}: missing outer_manifest after validation" | ||
| 156 | 163 | ||
| 157 | # Verify the fields exist now. | ||
| 158 | self.assertTrue( | ||
| 159 | hasattr(opts, "verbose"), | ||
| 160 | msg=f"{name}: missing verbose after validation", | ||
| 161 | ) | ||
| 162 | self.assertTrue( | ||
| 163 | hasattr(opts, "quiet"), | ||
| 164 | msg=f"{name}: missing quiet after validation", | ||
| 165 | ) | ||
| 166 | self.assertTrue( | ||
| 167 | hasattr(opts, "outer_manifest"), | ||
| 168 | msg=f"{name}: missing outer_manifest after validation", | ||
| 169 | ) | ||
| 170 | 164 | ||
| 171 | def test_attribute_error_repro(self): | 165 | def test_attribute_error_repro() -> None: |
| 172 | """Confirm that accessing verbose before CommonValidateOptions fails.""" | 166 | """Confirm that accessing verbose before CommonValidateOptions fails.""" |
| 173 | from subcmds.sync import Sync | 167 | from subcmds.sync import Sync |
| 174 | 168 | ||
| 175 | cmd = Sync() | 169 | cmd = Sync() |
| 176 | opts, args = cmd.OptionParser.parse_args([]) | 170 | opts, args = cmd.OptionParser.parse_args([]) |
| 177 | 171 | ||
| 178 | # This confirms that without the fix in main.py, an AttributeError | 172 | # This confirms that without the fix in main.py, an AttributeError |
| 179 | # would be raised because CommonValidateOptions hasn't been called yet. | 173 | # would be raised because CommonValidateOptions hasn't been called yet. |
| 180 | with self.assertRaises(AttributeError): | 174 | with pytest.raises(AttributeError): |
| 181 | _ = opts.verbose | 175 | _ = opts.verbose |
| 182 | 176 | ||
| 183 | cmd.CommonValidateOptions(opts, args) | 177 | cmd.CommonValidateOptions(opts, args) |
| 184 | self.assertTrue(hasattr(opts, "verbose")) | 178 | assert hasattr(opts, "verbose") |
diff --git a/tests/test_subcmds_init.py b/tests/test_subcmds_init.py index 25e5be567..960fb93fb 100644 --- a/tests/test_subcmds_init.py +++ b/tests/test_subcmds_init.py | |||
| @@ -14,33 +14,36 @@ | |||
| 14 | 14 | ||
| 15 | """Unittests for the subcmds/init.py module.""" | 15 | """Unittests for the subcmds/init.py module.""" |
| 16 | 16 | ||
| 17 | import unittest | 17 | from typing import List |
| 18 | |||
| 19 | import pytest | ||
| 18 | 20 | ||
| 19 | from subcmds import init | 21 | from subcmds import init |
| 20 | 22 | ||
| 21 | 23 | ||
| 22 | class InitCommand(unittest.TestCase): | 24 | @pytest.mark.parametrize( |
| 23 | """Check registered all_commands.""" | 25 | "argv", |
| 24 | 26 | ([],), | |
| 25 | def setUp(self): | 27 | ) |
| 26 | self.cmd = init.Init() | 28 | def test_cli_parser_good(argv: List[str]) -> None: |
| 27 | 29 | """Check valid command line options.""" | |
| 28 | def test_cli_parser_good(self): | 30 | cmd = init.Init() |
| 29 | """Check valid command line options.""" | 31 | opts, args = cmd.OptionParser.parse_args(argv) |
| 30 | ARGV = ([],) | 32 | cmd.ValidateOptions(opts, args) |
| 31 | for argv in ARGV: | 33 | |
| 32 | opts, args = self.cmd.OptionParser.parse_args(argv) | 34 | |
| 33 | self.cmd.ValidateOptions(opts, args) | 35 | @pytest.mark.parametrize( |
| 34 | 36 | "argv", | |
| 35 | def test_cli_parser_bad(self): | 37 | ( |
| 36 | """Check invalid command line options.""" | 38 | # Too many arguments. |
| 37 | ARGV = ( | 39 | ["url", "asdf"], |
| 38 | # Too many arguments. | 40 | # Conflicting options. |
| 39 | ["url", "asdf"], | 41 | ["--mirror", "--archive"], |
| 40 | # Conflicting options. | 42 | ), |
| 41 | ["--mirror", "--archive"], | 43 | ) |
| 42 | ) | 44 | def test_cli_parser_bad(argv: List[str]) -> None: |
| 43 | for argv in ARGV: | 45 | """Check invalid command line options.""" |
| 44 | opts, args = self.cmd.OptionParser.parse_args(argv) | 46 | cmd = init.Init() |
| 45 | with self.assertRaises(SystemExit): | 47 | opts, args = cmd.OptionParser.parse_args(argv) |
| 46 | self.cmd.ValidateOptions(opts, args) | 48 | with pytest.raises(SystemExit): |
| 49 | cmd.ValidateOptions(opts, args) | ||
diff --git a/tests/test_update_manpages.py b/tests/test_update_manpages.py index 06e5d3f5f..f52f8575f 100644 --- a/tests/test_update_manpages.py +++ b/tests/test_update_manpages.py | |||
| @@ -14,15 +14,10 @@ | |||
| 14 | 14 | ||
| 15 | """Unittests for the update_manpages module.""" | 15 | """Unittests for the update_manpages module.""" |
| 16 | 16 | ||
| 17 | import unittest | ||
| 18 | |||
| 19 | from release import update_manpages | 17 | from release import update_manpages |
| 20 | 18 | ||
| 21 | 19 | ||
| 22 | class UpdateManpagesTest(unittest.TestCase): | 20 | def test_replace_regex() -> None: |
| 23 | """Tests the update-manpages code.""" | 21 | """Check that replace_regex works.""" |
| 24 | 22 | data = "\n\033[1mSummary\033[m\n" | |
| 25 | def test_replace_regex(self): | 23 | assert update_manpages.replace_regex(data) == "\nSummary\n" |
| 26 | """Check that replace_regex works.""" | ||
| 27 | data = "\n\033[1mSummary\033[m\n" | ||
| 28 | self.assertEqual(update_manpages.replace_regex(data), "\nSummary\n") | ||
