diff options
Diffstat (limited to '')
-rw-r--r-- | tests/clash_path_file | 3 | ||||
-rw-r--r-- | tests/conftest.py | 26 | ||||
-rw-r--r-- | tests/empty_path_file | 0 | ||||
-rw-r--r-- | tests/mock_group_file | 2 | ||||
-rw-r--r-- | tests/mock_path_file | 4 | ||||
-rw-r--r-- | tests/test_info.py | 16 | ||||
-rw-r--r-- | tests/test_main.py | 167 | ||||
-rw-r--r-- | tests/test_utils.py | 118 |
8 files changed, 336 insertions, 0 deletions
diff --git a/tests/clash_path_file b/tests/clash_path_file new file mode 100644 index 0000000..4abbfca --- /dev/null +++ b/tests/clash_path_file @@ -0,0 +1,3 @@ +/a/bcd/repo1,repo1 +/e/fgh/repo2,repo2 +/root/x/repo1,repo1 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b3e59ed --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,26 @@ +from pathlib import Path +from unittest.mock import MagicMock + +TEST_DIR = Path(__file__).parents[0] + + +def fullpath(fname: str): + return str(TEST_DIR / fname) + + +PATH_FNAME = fullpath('mock_path_file') +PATH_FNAME_EMPTY = fullpath('empty_path_file') +PATH_FNAME_CLASH = fullpath('clash_path_file') +GROUP_FNAME = fullpath('mock_group_file') + +def async_mock(): + """ + Mock an async function. The calling arguments are saved in a MagicMock. + """ + m = MagicMock() + + async def coro(*args, **kwargs): + return m(*args, **kwargs) + + coro.mock = m + return coro diff --git a/tests/empty_path_file b/tests/empty_path_file new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/empty_path_file diff --git a/tests/mock_group_file b/tests/mock_group_file new file mode 100644 index 0000000..32f0a64 --- /dev/null +++ b/tests/mock_group_file @@ -0,0 +1,2 @@ +xx: [a, b] +yy: [a, c, d] diff --git a/tests/mock_path_file b/tests/mock_path_file new file mode 100644 index 0000000..2a5f9f9 --- /dev/null +++ b/tests/mock_path_file @@ -0,0 +1,4 @@ +/a/bcd/repo1,repo1 +/a/b/c/repo3,xxx +/e/fgh/repo2,repo2 + diff --git a/tests/test_info.py b/tests/test_info.py new file mode 100644 index 0000000..025aedc --- /dev/null +++ b/tests/test_info.py @@ -0,0 +1,16 @@ +import subprocess +from unittest.mock import patch, MagicMock + +from gita import info + + +@patch('subprocess.run') +def test_run_quiet_diff(mock_run): + mock_return = MagicMock() + mock_run.return_value = mock_return + got = info.run_quiet_diff(['my', 'args']) + mock_run.assert_called_once_with( + ['git', 'diff', '--quiet', 'my', 'args'], + stderr=subprocess.DEVNULL, + ) + assert got == mock_return.returncode diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..1946352 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,167 @@ +import pytest +from unittest.mock import patch +import argparse +import shlex + +from gita import __main__ +from gita import utils +from conftest import ( + PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME, + async_mock +) + + +class TestLsLl: + @patch('gita.utils.get_config_fname') + def testLl(self, mock_path_fname, capfd, tmp_path): + """ functional test """ + # avoid modifying the local configuration + mock_path_fname.return_value = tmp_path / 'path_config.txt' + __main__.main(['add', '.']) + out, err = capfd.readouterr() + assert err == '' + assert 'Found 1 new repo(s).\n' == out + + # in production this is not needed + utils.get_repos.cache_clear() + + __main__.main(['ls']) + out, err = capfd.readouterr() + assert err == '' + assert 'gita\n' == out + + __main__.main(['ll']) + out, err = capfd.readouterr() + assert err == '' + assert 'gita' in out + + __main__.main(['ls', 'gita']) + out, err = capfd.readouterr() + assert err == '' + assert out.strip() == utils.get_repos()['gita'] + + def testLs(self, monkeypatch, capfd): + monkeypatch.setattr(utils, 'get_repos', + lambda: {'repo1': '/a/', 'repo2': '/b/'}) + monkeypatch.setattr(utils, 'describe', lambda x: x) + __main__.main(['ls']) + out, err = capfd.readouterr() + assert err == '' + assert out == "repo1 repo2\n" + __main__.main(['ls', 'repo1']) + out, err = capfd.readouterr() + assert err == '' + assert out == '/a/\n' + + @pytest.mark.parametrize('path_fname, expected', [ + (PATH_FNAME, + "repo1 cmaster dsu\x1b[0m msg\nrepo2 cmaster dsu\x1b[0m msg\nxxx cmaster dsu\x1b[0m msg\n"), + (PATH_FNAME_EMPTY, ""), + (PATH_FNAME_CLASH, + "repo1 cmaster dsu\x1b[0m msg\nrepo2 cmaster dsu\x1b[0m msg\nx/repo1 cmaster dsu\x1b[0m msg\n" + ), + ]) + @patch('gita.utils.is_git', return_value=True) + @patch('gita.info.get_head', return_value="master") + @patch('gita.info._get_repo_status', return_value=("d", "s", "u", "c")) + @patch('gita.info.get_commit_msg', return_value="msg") + @patch('gita.utils.get_config_fname') + def testWithPathFiles(self, mock_path_fname, _0, _1, _2, _3, path_fname, + expected, capfd): + mock_path_fname.return_value = path_fname + utils.get_repos.cache_clear() + __main__.main(['ll']) + out, err = capfd.readouterr() + print(out) + assert err == '' + assert out == expected + + +@patch('os.path.isfile', return_value=True) +@patch('gita.utils.get_config_fname', return_value='some path') +@patch('gita.utils.get_repos', return_value={'repo1': '/a/', 'repo2': '/b/'}) +@patch('gita.utils.write_to_repo_file') +def test_rm(mock_write, *_): + args = argparse.Namespace() + args.repo = ['repo1'] + __main__.f_rm(args) + mock_write.assert_called_once_with({'repo2': '/b/'}, 'w') + + +def test_not_add(): + # this won't write to disk because the repo is not valid + __main__.main(['add', '/home/some/repo/']) + + +@patch('gita.utils.get_repos', return_value={'repo2': '/d/efg'}) +@patch('subprocess.run') +def test_fetch(mock_run, *_): + __main__.main(['fetch']) + mock_run.assert_called_once_with(['git', 'fetch'], cwd='/d/efg') + + +@patch( + 'gita.utils.get_repos', return_value={ + 'repo1': '/a/bc', + 'repo2': '/d/efg' + }) +@patch('gita.utils.run_async', new=async_mock()) +@patch('subprocess.run') +def test_async_fetch(*_): + __main__.main(['fetch']) + mock_run = utils.run_async.mock + assert mock_run.call_count == 2 + cmds = ['git', 'fetch'] + # print(mock_run.call_args_list) + mock_run.assert_any_call('repo1', '/a/bc', cmds) + mock_run.assert_any_call('repo2', '/d/efg', cmds) + + +@pytest.mark.parametrize('input', [ + 'diff --name-only --staged', + "commit -am 'lala kaka'", +]) +@patch('gita.utils.get_repos', return_value={'repo7': 'path7'}) +@patch('subprocess.run') +def test_superman(mock_run, _, input): + mock_run.reset_mock() + args = ['super', 'repo7'] + shlex.split(input) + __main__.main(args) + expected_cmds = ['git'] + shlex.split(input) + mock_run.assert_called_once_with(expected_cmds, cwd='path7') + + +@pytest.mark.parametrize('input, expected', [ + ('a', {'xx': ['b'], 'yy': ['c', 'd']}), + ("c", {'xx': ['a', 'b'], 'yy': ['a', 'd']}), + ("a b", {'yy': ['c', 'd']}), +]) +@patch('gita.utils.get_repos', return_value={'a': '', 'b': '', 'c': '', 'd': ''}) +@patch('gita.utils.get_config_fname', return_value=GROUP_FNAME) +@patch('gita.utils.write_to_groups_file') +def test_ungroup(mock_write, _, __, input, expected): + utils.get_groups.cache_clear() + args = ['ungroup'] + shlex.split(input) + __main__.main(args) + mock_write.assert_called_once_with(expected, 'w') + + +@patch('gita.utils.is_git', return_value=True) +@patch('gita.utils.get_config_fname', return_value=PATH_FNAME) +@patch('gita.utils.rename_repo') +def test_rename(mock_rename, _, __): + utils.get_repos.cache_clear() + args = ['rename', 'repo1', 'abc'] + __main__.main(args) + mock_rename.assert_called_once_with( + {'repo1': '/a/bcd/repo1', 'repo2': '/e/fgh/repo2', + 'xxx': '/a/b/c/repo3'}, + 'repo1', 'abc') + + +@patch('os.path.isfile', return_value=False) +def test_info(mock_isfile, capfd): + __main__.f_info(None) + out, err = capfd.readouterr() + assert 'In use: branch,commit_msg\nUnused: path\n' == out + assert err == '' diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..3128041 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,118 @@ +import pytest +import asyncio +from unittest.mock import patch, mock_open + +from gita import utils, info +from conftest import ( + PATH_FNAME, PATH_FNAME_EMPTY, PATH_FNAME_CLASH, GROUP_FNAME, +) + + +@pytest.mark.parametrize('test_input, diff_return, expected', [ + ({ + 'abc': '/root/repo/' + }, True, 'abc \x1b[31mrepo *+_ \x1b[0m msg'), + ({ + 'repo': '/root/repo2/' + }, False, 'repo \x1b[32mrepo _ \x1b[0m msg'), +]) +def test_describe(test_input, diff_return, expected, monkeypatch): + monkeypatch.setattr(info, 'get_head', lambda x: 'repo') + monkeypatch.setattr(info, 'run_quiet_diff', lambda _: diff_return) + monkeypatch.setattr(info, 'get_commit_msg', lambda _: "msg") + monkeypatch.setattr(info, 'has_untracked', lambda: True) + monkeypatch.setattr('os.chdir', lambda x: None) + print('expected: ', repr(expected)) + print('got: ', repr(next(utils.describe(test_input)))) + assert expected == next(utils.describe(test_input)) + + +@pytest.mark.parametrize('path_fname, expected', [ + (PATH_FNAME, { + 'repo1': '/a/bcd/repo1', + 'repo2': '/e/fgh/repo2', + 'xxx': '/a/b/c/repo3', + }), + (PATH_FNAME_EMPTY, {}), + (PATH_FNAME_CLASH, { + 'repo1': '/a/bcd/repo1', + 'repo2': '/e/fgh/repo2', + 'x/repo1': '/root/x/repo1' + }), +]) +@patch('gita.utils.is_git', return_value=True) +@patch('gita.utils.get_config_fname') +def test_get_repos(mock_path_fname, _, path_fname, expected): + mock_path_fname.return_value = path_fname + utils.get_repos.cache_clear() + assert utils.get_repos() == expected + + +@pytest.mark.parametrize('group_fname, expected', [ + (GROUP_FNAME, {'xx': ['a', 'b'], 'yy': ['a', 'c', 'd']}), +]) +@patch('gita.utils.get_config_fname') +def test_get_groups(mock_group_fname, group_fname, expected): + mock_group_fname.return_value = group_fname + utils.get_groups.cache_clear() + assert utils.get_groups() == expected + + +@patch('os.path.isfile', return_value=True) +@patch('os.path.getsize', return_value=True) +def test_custom_push_cmd(*_): + with patch('builtins.open', + mock_open(read_data='push:\n cmd: hand\n help: me')): + cmds = utils.get_cmds_from_files() + assert cmds['push'] == {'cmd': 'hand', 'help': 'me'} + + +@pytest.mark.parametrize( + 'path_input, expected', + [ + (['/home/some/repo/'], '/home/some/repo,repo\n'), # add one new + (['/home/some/repo1', '/repo2'], + {'/repo2,repo2\n/home/some/repo1,repo1\n', # add two new + '/home/some/repo1,repo1\n/repo2,repo2\n'}), # add two new + (['/home/some/repo1', '/nos/repo'], + '/home/some/repo1,repo1\n'), # add one old one new + ]) +@patch('os.makedirs') +@patch('gita.utils.is_git', return_value=True) +def test_add_repos(_0, _1, path_input, expected, monkeypatch): + monkeypatch.setenv('XDG_CONFIG_HOME', '/config') + with patch('builtins.open', mock_open()) as mock_file: + utils.add_repos({'repo': '/nos/repo'}, path_input) + mock_file.assert_called_with('/config/gita/repo_path', 'a+') + handle = mock_file() + if type(expected) == str: + handle.write.assert_called_once_with(expected) + else: + handle.write.assert_called_once() + args, kwargs = handle.write.call_args + assert args[0] in expected + assert not kwargs + + +@patch('gita.utils.write_to_repo_file') +def test_rename_repo(mock_write): + utils.rename_repo({'r1': '/a/b', 'r2': '/c/c'}, 'r2', 'xxx') + mock_write.assert_called_once_with({'r1': '/a/b', 'xxx': '/c/c'}, 'w') + + +def test_async_output(capfd): + tasks = [ + utils.run_async('myrepo', '.', [ + 'python3', '-c', + f"print({i});import time; time.sleep({i});print({i})" + ]) for i in range(4) + ] + # I don't fully understand why a new loop is needed here. Without a new + # loop, "pytest" fails but "pytest tests/test_utils.py" works. Maybe pytest + # itself uses asyncio (or maybe pytest-xdist)? + asyncio.set_event_loop(asyncio.new_event_loop()) + utils.exec_async_tasks(tasks) + + out, err = capfd.readouterr() + assert err == '' + assert out == 'myrepo: 0\nmyrepo: 0\n\nmyrepo: 1\nmyrepo: 1\n\nmyrepo: 2\nmyrepo: 2\n\nmyrepo: 3\nmyrepo: 3\n\n' |