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:
Marc Cataford 2020-01-06 23:00:22 -05:00 committed by GitHub
parent 26058edc86
commit c647f9dbd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 270 additions and 14 deletions

View file

@ -23,3 +23,6 @@ jobs:
- name: Formatter
run: |
black src *.py --check
- name: Test
run: |
pytest

2
.gitignore vendored
View file

@ -4,8 +4,6 @@ __pycache__/
*$py.class
*.sw[a-z]
#Temporarily
tests/
# C extensions
*.so

View file

@ -6,4 +6,5 @@ pyenv install -s
pyenv virtualenv $VENV_NAME
pyenv activate $VENV_NAME
pip install wheel twine
pip install -r $REQ_FILE

View 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$"

View file

@ -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__))

View file

@ -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"])))

View file

@ -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
View file

View 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
View 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
View 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