summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/clash_path_file3
-rw-r--r--tests/conftest.py26
-rw-r--r--tests/empty_path_file0
-rw-r--r--tests/mock_group_file2
-rw-r--r--tests/mock_path_file4
-rw-r--r--tests/test_info.py16
-rw-r--r--tests/test_main.py167
-rw-r--r--tests/test_utils.py118
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'