Test coverage pt.1 (#4)
* test: config fetch coverage * ci: add test runner * test: add test stubs * chore: move twine and wheel to bootstrap * infra: add test inv * refactor: review for testability * test: use case testing
This commit is contained in:
parent
26058edc86
commit
c647f9dbd9
11 changed files with 270 additions and 14 deletions
3
.github/workflows/pythonpackage.yml
vendored
3
.github/workflows/pythonpackage.yml
vendored
|
@ -23,3 +23,6 @@ jobs:
|
|||
- name: Formatter
|
||||
run: |
|
||||
black src *.py --check
|
||||
- name: Test
|
||||
run: |
|
||||
pytest
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,8 +4,6 @@ __pycache__/
|
|||
*$py.class
|
||||
*.sw[a-z]
|
||||
|
||||
#Temporarily
|
||||
tests/
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
|
|
|
@ -6,4 +6,5 @@ pyenv install -s
|
|||
pyenv virtualenv $VENV_NAME
|
||||
pyenv activate $VENV_NAME
|
||||
|
||||
pip install wheel twine
|
||||
pip install -r $REQ_FILE
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
RCFILE_PATH = ".carboncopyrc"
|
||||
|
||||
FORCED_IGNORE_PATTERNS = r"^.git/*"
|
||||
FORCED_IGNORE_PATTERNS = r".*\.git/.*"
|
||||
FETCH_URL_PATTERN = r"Fetch URL"
|
||||
GIT_LINK_PATTERN = r"(?<=git@github.com:)[A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+"
|
||||
GIT_EXT_PATTERN = r"\.git$"
|
||||
|
|
|
@ -38,7 +38,7 @@ def get_template_transforms(path: Path) -> List[Transform]:
|
|||
while stack:
|
||||
current_path = stack.pop()
|
||||
|
||||
if not current_path.is_dir():
|
||||
if not Path(current_path).is_dir():
|
||||
file_paths.append(current_path)
|
||||
continue
|
||||
|
||||
|
@ -47,7 +47,10 @@ def get_template_transforms(path: Path) -> List[Transform]:
|
|||
stack.append(child_path)
|
||||
|
||||
return [
|
||||
Transform(source=filename, destination=filename.relative_to(path))
|
||||
Transform(
|
||||
source=filename,
|
||||
destination=path.parent.joinpath(filename.relative_to(path)),
|
||||
)
|
||||
for filename in file_paths
|
||||
]
|
||||
|
||||
|
@ -69,9 +72,8 @@ def squash(transform: Transform) -> None:
|
|||
except IsADirectoryError:
|
||||
pretty_print(
|
||||
"Failed to copy {source} -> {destination}".format(
|
||||
source=source, destination=destination
|
||||
source=source, destination=destination,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
pretty_print(e.__class__)
|
||||
pretty_print(e)
|
||||
pretty_print(str(e.__class__))
|
||||
|
|
|
@ -46,19 +46,24 @@ def get_local_config(root_path: Path = Path(".")) -> Dict[str, Any]:
|
|||
|
||||
|
||||
class UseCases:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
def __init__(
|
||||
self, config: Dict[str, Any], non_interactive=False, root_path=Path(".")
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.template_repo: Dict[str, str] = {}
|
||||
self.org = None
|
||||
self.repo = None
|
||||
self.non_interactive = non_interactive
|
||||
self.root_path = root_path
|
||||
|
||||
def fetch_template_repository_details(self) -> None:
|
||||
org, repo = get_local_repository_meta()
|
||||
template_repo_data = get_repo_metadata(org, repo)
|
||||
|
||||
if not (org and repo):
|
||||
raise NotInAGitRepositoryError()
|
||||
|
||||
template_repo_data = get_repo_metadata(org, repo)
|
||||
|
||||
if not template_repo_data:
|
||||
raise NoTemplateError()
|
||||
|
||||
|
@ -72,13 +77,16 @@ class UseCases:
|
|||
)
|
||||
|
||||
def stage_changes(self) -> List[Transform]:
|
||||
path = Path(self.config["temp_directory"])
|
||||
path = self.root_path.joinpath(Path(self.config["temp_directory"]))
|
||||
available_transforms = get_template_transforms(path)
|
||||
|
||||
def can_stage(path_str: str) -> bool:
|
||||
return not re.match(FORCED_IGNORE_PATTERNS, path_str) and all(
|
||||
[re.match(patt, path_str) for patt in self.config["ignore"]]
|
||||
is_forced_ignore = re.search(FORCED_IGNORE_PATTERNS, path_str)
|
||||
is_custom_ignore = any(
|
||||
[re.search(patt, path_str) for patt in self.config["ignore"]]
|
||||
)
|
||||
is_dir = Path(path_str).is_dir()
|
||||
return not is_dir and not is_custom_ignore and not is_forced_ignore
|
||||
|
||||
allowed_transforms = [
|
||||
transform
|
||||
|
@ -86,12 +94,17 @@ class UseCases:
|
|||
if can_stage(transform.get_destination(as_str=True))
|
||||
]
|
||||
|
||||
if self.non_interactive:
|
||||
return allowed_transforms
|
||||
|
||||
destinations = [
|
||||
transform.get_destination(as_str=True) for transform in allowed_transforms
|
||||
]
|
||||
|
||||
chosen_files = prompt_staging_files_confirmation(
|
||||
destinations, "{}/{}".format(self.org, self.repo)
|
||||
)
|
||||
|
||||
chosen_transforms = [
|
||||
transform
|
||||
for transform in allowed_transforms
|
||||
|
@ -106,4 +119,4 @@ class UseCases:
|
|||
squash(path)
|
||||
|
||||
def clean_up(self) -> None:
|
||||
clean_temp_files(Path(self.config["temp_directory"]))
|
||||
clean_temp_files(self.root_path.joinpath(Path(self.config["temp_directory"])))
|
||||
|
|
8
tasks.py
8
tasks.py
|
@ -1,5 +1,7 @@
|
|||
from invoke import Collection, task
|
||||
|
||||
TMP_PATH = "/tmp/carboncopy_pytest/"
|
||||
|
||||
|
||||
@task
|
||||
def format_all(ctx):
|
||||
|
@ -26,8 +28,14 @@ def publish(ctx, test=False):
|
|||
ctx.run("twine upload dist/*")
|
||||
|
||||
|
||||
@task
|
||||
def test(ctx):
|
||||
ctx.run("pytest")
|
||||
|
||||
|
||||
ns = Collection()
|
||||
ns.add_task(format_all, name="format")
|
||||
ns.add_task(typecheck)
|
||||
ns.add_task(package)
|
||||
ns.add_task(publish)
|
||||
ns.add_task(test)
|
||||
|
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
54
tests/__snapshots__/test_use_cases.ambr
Normal file
54
tests/__snapshots__/test_use_cases.ambr
Normal file
|
@ -0,0 +1,54 @@
|
|||
# name: test_fetch_template_repository_details_throws_NoTemplateError_if_no_template_repo
|
||||
''
|
||||
---
|
||||
# name: test_fetch_template_repository_details_throws_NoTemplateError_if_no_template_repo.1
|
||||
''
|
||||
---
|
||||
# name: test_fetch_template_repository_details_throws_NotInGitRepositoryError_if_not_in_repo
|
||||
''
|
||||
---
|
||||
# name: test_fetch_template_repository_details_throws_NotInGitRepositoryError_if_not_in_repo.1
|
||||
''
|
||||
---
|
||||
# name: test_get_local_config_merges_rcfile_with_default_config
|
||||
''
|
||||
---
|
||||
# name: test_get_local_config_merges_rcfile_with_default_config.1
|
||||
''
|
||||
---
|
||||
# name: test_get_local_config_returns_default_config_if_invalid_config_file_present
|
||||
'
|
||||
Invalid RC file!
|
||||
|
||||
'
|
||||
---
|
||||
# name: test_get_local_config_returns_default_config_if_invalid_config_file_present.1
|
||||
''
|
||||
---
|
||||
# name: test_get_local_config_returns_default_config_if_no_config_file_present
|
||||
'
|
||||
No config file found in current directory! Proceeding with defaults.
|
||||
|
||||
'
|
||||
---
|
||||
# name: test_get_local_config_returns_default_config_if_no_config_file_present.1
|
||||
''
|
||||
---
|
||||
# name: test_stage_changes_creates_transforms_for_all_valid_changes
|
||||
''
|
||||
---
|
||||
# name: test_stage_changes_creates_transforms_for_all_valid_changes.1
|
||||
''
|
||||
---
|
||||
# name: test_stage_changes_ignores_all_configured_ignore_patterns
|
||||
''
|
||||
---
|
||||
# name: test_stage_changes_ignores_all_configured_ignore_patterns.1
|
||||
''
|
||||
---
|
||||
# name: test_stage_changes_ignores_all_forced_ignore_patterns
|
||||
''
|
||||
---
|
||||
# name: test_stage_changes_ignores_all_forced_ignore_patterns.1
|
||||
''
|
||||
---
|
170
tests/test_use_cases.py
Normal file
170
tests/test_use_cases.py
Normal file
|
@ -0,0 +1,170 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.carboncopy.use_cases import get_local_config, UseCases
|
||||
from src.carboncopy.constants import RCFILE_PATH
|
||||
from src.carboncopy.config_defaults import CONFIG_DEFAULTS
|
||||
from src.carboncopy.git_utils import NotInAGitRepositoryError, NoTemplateError
|
||||
|
||||
from .test_utils import assert_captured_output_matches_snapshot
|
||||
|
||||
def test_get_local_config_returns_default_config_if_no_config_file_present(tmp_path, snapshot, capsys):
|
||||
assert len(list(tmp_path.iterdir())) == 0
|
||||
|
||||
fetched_config = get_local_config(tmp_path)
|
||||
|
||||
assert fetched_config == CONFIG_DEFAULTS
|
||||
|
||||
assert_captured_output_matches_snapshot(capsys, snapshot)
|
||||
|
||||
def test_get_local_config_returns_default_config_if_invalid_config_file_present(tmp_path, snapshot, capsys):
|
||||
invalid_config_file = tmp_path / RCFILE_PATH
|
||||
|
||||
invalid_config_file.write_text('')
|
||||
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
|
||||
fetched_config = get_local_config(tmp_path)
|
||||
|
||||
assert fetched_config == CONFIG_DEFAULTS
|
||||
|
||||
assert_captured_output_matches_snapshot(capsys, snapshot)
|
||||
|
||||
def test_get_local_config_merges_rcfile_with_default_config(tmp_path, snapshot, capsys):
|
||||
valid_config = { "ignore": ["some-file.md"] }
|
||||
|
||||
config_file = tmp_path / RCFILE_PATH
|
||||
config_file.write_text(json.dumps(valid_config))
|
||||
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
|
||||
fetched_config = get_local_config(tmp_path)
|
||||
|
||||
expected_config = {**CONFIG_DEFAULTS, **valid_config}
|
||||
|
||||
assert fetched_config == expected_config
|
||||
|
||||
assert_captured_output_matches_snapshot(capsys, snapshot)
|
||||
|
||||
def test_fetch_template_repository_details_throws_NotInGitRepositoryError_if_not_in_repo(capsys, snapshot, monkeypatch):
|
||||
# This simulates the repository meta not finding a repository.
|
||||
def _mock():
|
||||
return (None, None)
|
||||
|
||||
monkeypatch.setattr("src.carboncopy.use_cases.get_local_repository_meta", _mock)
|
||||
|
||||
use_cases = UseCases(config=CONFIG_DEFAULTS)
|
||||
|
||||
with pytest.raises(NotInAGitRepositoryError):
|
||||
use_cases.fetch_template_repository_details()
|
||||
|
||||
assert_captured_output_matches_snapshot(capsys, snapshot)
|
||||
|
||||
def test_fetch_template_repository_details_throws_NoTemplateError_if_no_template_repo(capsys, monkeypatch, snapshot):
|
||||
# This simulates the repository data not containing a template repo reference
|
||||
def _mock(a, b):
|
||||
return None
|
||||
|
||||
def _mock_local_meta():
|
||||
return 'org', 'repo'
|
||||
|
||||
monkeypatch.setattr('src.carboncopy.use_cases.get_local_repository_meta', _mock_local_meta)
|
||||
monkeypatch.setattr("src.carboncopy.use_cases.get_repo_metadata", _mock)
|
||||
|
||||
use_cases = UseCases(config=CONFIG_DEFAULTS, non_interactive=True)
|
||||
|
||||
with pytest.raises(NoTemplateError):
|
||||
use_cases.fetch_template_repository_details()
|
||||
|
||||
assert_captured_output_matches_snapshot(capsys, snapshot)
|
||||
|
||||
def test_stage_changes_ignores_all_forced_ignore_patterns(capsys, tmp_path, snapshot):
|
||||
# Set up a mock template_directory
|
||||
temp_dir = tmp_path / CONFIG_DEFAULTS["temp_directory"]
|
||||
temp_dir.mkdir()
|
||||
# .git is a notoriously ignored directory
|
||||
forced_ignored_dir = temp_dir / ".git"
|
||||
forced_ignored_dir.mkdir()
|
||||
some_file = forced_ignored_dir / "some_file.txt"
|
||||
some_file.write_text("smol file")
|
||||
|
||||
use_cases = UseCases(config=CONFIG_DEFAULTS, non_interactive=True, root_path=tmp_path)
|
||||
|
||||
staged = use_cases.stage_changes()
|
||||
assert len(staged) == 0
|
||||
|
||||
assert_captured_output_matches_snapshot(capsys, snapshot)
|
||||
|
||||
|
||||
def test_stage_changes_ignores_all_configured_ignore_patterns(capsys, tmp_path, snapshot):
|
||||
# Set up a mock template_directory
|
||||
temp_dir = tmp_path / CONFIG_DEFAULTS["temp_directory"]
|
||||
temp_dir.mkdir()
|
||||
forced_ignored_dir = temp_dir / "ignore_folder"
|
||||
forced_ignored_dir.mkdir()
|
||||
some_file = forced_ignored_dir / "some_file.txt"
|
||||
some_file.write_text("smol file")
|
||||
|
||||
config = { "ignore": ["ignore_folder"] }
|
||||
merged_config = {**CONFIG_DEFAULTS, **config}
|
||||
use_cases = UseCases(config=merged_config , non_interactive=True, root_path=tmp_path)
|
||||
|
||||
staged = use_cases.stage_changes()
|
||||
assert len(staged) == 0
|
||||
|
||||
assert_captured_output_matches_snapshot(capsys, snapshot)
|
||||
|
||||
def test_stage_changes_creates_transforms_for_all_valid_changes(capsys, snapshot, tmp_path):
|
||||
# Set up a mock template_directory
|
||||
temp_dir = tmp_path / CONFIG_DEFAULTS["temp_directory"]
|
||||
temp_dir.mkdir()
|
||||
forced_ignored_dir = temp_dir / "coolio"
|
||||
forced_ignored_dir.mkdir()
|
||||
some_file = forced_ignored_dir / "some_file.txt"
|
||||
some_file.write_text("smol file")
|
||||
|
||||
use_cases = UseCases(config=CONFIG_DEFAULTS , non_interactive=True, root_path=tmp_path)
|
||||
|
||||
staged = use_cases.stage_changes()
|
||||
assert len(staged) == 1
|
||||
|
||||
assert_captured_output_matches_snapshot(capsys, snapshot)
|
||||
|
||||
|
||||
|
||||
def test_apply_changes_squashes_all_files(tmp_path, capsys, snapshot):
|
||||
temp_dir = tmp_path / CONFIG_DEFAULTS["temp_directory"]
|
||||
temp_dir.mkdir()
|
||||
cloned_repo = temp_dir / "attack_of_the_clone_repos"
|
||||
cloned_repo.mkdir()
|
||||
some_file = cloned_repo / "some_file.txt"
|
||||
some_file.write_text("smol file")
|
||||
|
||||
use_cases = UseCases(config=CONFIG_DEFAULTS, non_interactive=True, root_path=tmp_path)
|
||||
|
||||
staged = use_cases.stage_changes()
|
||||
use_cases.apply_changes(staged)
|
||||
|
||||
resulting_files = list(tmp_path.iterdir())
|
||||
|
||||
resulting_copy = tmp_path / "attack_of_the_clone_repos"
|
||||
assert len(list(resulting_copy.iterdir())) == 1
|
||||
|
||||
copied_file = resulting_copy / "some_file.txt"
|
||||
|
||||
assert copied_file.read_text() == "smol file"
|
||||
|
||||
def test_clean_up_cleans_up_temporary_directory_files(tmp_path):
|
||||
temp_dir = tmp_path / CONFIG_DEFAULTS["temp_directory"]
|
||||
temp_dir.mkdir()
|
||||
cloned_repo = temp_dir / "attack_of_the_clone_repos"
|
||||
cloned_repo.mkdir()
|
||||
some_file = cloned_repo / "some_file.txt"
|
||||
some_file.write_text("smol file")
|
||||
|
||||
use_cases = UseCases(config=CONFIG_DEFAULTS, non_interactive=True, root_path=tmp_path)
|
||||
|
||||
use_cases.clean_up()
|
||||
|
||||
assert len(list(tmp_path.iterdir())) == 0
|
7
tests/test_utils.py
Normal file
7
tests/test_utils.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import pytest
|
||||
|
||||
def assert_captured_output_matches_snapshot(capsys, snapshot):
|
||||
captured_out = capsys.readouterr()
|
||||
|
||||
assert captured_out.out == snapshot
|
||||
assert captured_out.err == snapshot
|
Reference in a new issue