diff --git a/README.md b/README.md index bce1fea..eda64cd 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,9 @@ # repo-smith -YAML-based configuration for Git repository initialization for unit testing +Declarative syntax for initializing Git repositories. ## Installation ```bash pip install -U repo-smith ``` - -## Usage - -Create a new configuration file (following the [specification](/specification.md)). - -```yml -# File path: tests/specs/basic_spec.yml -name: Basic spec -description: Starting basic spec -initialization: - steps: - - name: Create filea.txt - type: new-file - filename: filea.txt - contents: | - Hello world - - name: Add filea.txt - type: add - files: - - filea.txt - - name: Initial commit - type: commit - message: Initial commit - id: initial-commit - - name: v0 tag - type: tag - tag-name: v0 -``` - -Create a unit test accordingly. - -```py -from repo_smith.initialize_repo import initialize_repo -from git import Repo - -def test_dummy(): - repo_initializer = initialize_repo("test/specs/basic_spec.yml") - with repo_initializer.initialize() as repo: - # All unit testing code for the dummy repository goes here - print(repo) -``` - -For more use cases of `repo-smith`, refer to: - -- [Official specification](/specification.md) -- [Unit tests](./tests/) - -## FAQ - -### Why don't you assign every error to a constant and unit test against the constant? - -Suppose the constant was `X`, and we unit tested that the error value was `X`, we would not capture any nuances if the value of `X` had changed by accident. diff --git a/specification.md b/specification.md deleted file mode 100644 index bca47df..0000000 --- a/specification.md +++ /dev/null @@ -1,376 +0,0 @@ -# `repo-smith` - -## Inspiration - -`repo-smith` is a unit testing library built on top of `GitPython` and -inspired by the -[`GitPython` unit testing](https://github.com/gitpython-developers/GitPython/blob/main/test/test_diff.py) -where unit tests for Git repositories are performed by directly creating -temporary directories and initializing them as Git repositories. - -The process of writing the necessary code to create the Git repository (before -any unit testing can even be conducted) is too tedious and given that we want -to similarly unit test solution files for Git Mastery, we want a much better -overall developer experience in initializing and creating test Git repositories. - -`repo-smith` declares a lightweight YAML-based configuration language, -allowing developers to declare Git repositories to be created using an -intuitive syntax (detailed below) and will serve as the basis for initializing -larger and more complex repositories. Using `repo-smith`, you can streamline -your Git repository unit testing, focusing on validating the behavior of your -solutions. - -## Syntax - -### `name` - -Name of the Git repository unit test. Optional. - -Type: `string` - -### `description` - -Description of the Git repository unit test. Optional. - -Type: `string` - -### `initialization` - -Includes the instructions and components to initialize the repository. - -> [!NOTE] -> All steps are run sequentially, so ensure that you are declaring the -> repository from top to bottom - -#### `initialization.clone-from` - -Specifies a base repository to clone and start the initialization with. - -Type: `string` - -```yml -initialization: - clone-from: https://github.com/git-mastery/repo-smith - steps: - - type: commit - empty: true - message: Empty commit -``` - -The above will clone the `git-mastery/repo-smith` repository and add a new -commit in it. - -#### `initialization.steps[*].name` - -Name of the initialization step. Optional. - -Type: `string` - -#### `initialization.steps[*].description` - -Description of the initialization step. Optional. - -Type: `string` - -#### `initialization.steps[*].id` - -Provides a unique `id` for the current step. Optional, but if provided, will be -validated to ensure uniqueness across `initialization.steps[*]`. - -When a step is provided with an `id`, hooks will be automatically installed and -events for the hook will be pushed. More information about the -[lifecycle hooks here.](#lifecycle-hooks) - -Type: `string` - -Constraints: - -- Only alphanumeric characters, `-`, and `_` -- No spaces allowed - -#### `initialization.steps[*].type` - -Type of action of the step. Not optional. - -Accepted values include: - -- `commit` -- `add` -- `tag` -- `new-file` -- `edit-file` -- `delete-file` -- `append-file` -- `bash` -- `branch` -- `checkout` -- `remote` -- `reset` -- `revert` -- `merge` -- `fetch` -- `branch-rename` -- `branch-delete` - - -#### `initialization.steps[*].empty` - -Indicates if the commit should be empty. Only read if -`initialization.steps[*].type` is `commit` - -Type: `bool` - -#### `initialization.steps[*].message` - -Commit message to be used. Only read if `initialization.steps[*].type` is `commit`. - -Type: `string` - -#### `initialization.steps[*].files` - -File names to be added or reset. Read if `initialization.steps[*].type` is -`add` or `reset`. - -For `add`: Files to add to the staging area. - -For `reset`: Specific files to reset in the staging area (optional). When -`files` is provided with reset, only the specified files are unstaged. -`mode` must be `mixed`. - -Type: `list` - -#### `initialization.steps[*].tag-name` - -Tag name to be used on the current commit. Only read if -`initialization.steps[*].type` is `tag`. - -The tag name cannot be duplicated, otherwise, the framework will throw an -exception during initialization. - -Type: `string` - -#### `initialization.steps[*].tag-message` - -Tag message to be used on the current commit. Only read if -`initialization.steps[*].type` is `tag`. Optional. - -Type: `string` - -#### `initialization.steps[*].filename` - -Target file name. Only read if -`initialization.steps[*].type` is `new-file`, `edit-file`, `delete-file` or -`append-file`. - -Specify any new folders in the `filename` and `repo-smith` will initialize them -accordingly. - -Type: `string` - -#### `initialization.steps[*].contents` - -File content for file `initialization.steps[*].filename`. Only read if -`initialization.steps[*].type` is `new-file`, `edit-file`, `delete-file` or -`append-file`. - -Type: `string` - -#### `initialization.steps[*].runs` - -Bash commands to execute. Only read if `initialization.steps[*].type` is `bash`. - -Type: `string` - -#### `initialization.steps[*].branch-name` - -Branch name. Only read if `initialization.steps[*].type` is `branch` or -`checkout` or `merge` or `branch-rename` or `branch-delete`. - -Users are expected to manage their own branches themselves. - -Type: `string` - -#### `initialization.steps[*].new-name` - -New branch name. Only read if `initialization.steps[*].type` is `branch-rename`. - -Type: `string` - -#### `initialization.steps[*].no-ff` - -Whether the merge will use fast-forwarding. Only read if -`initialization.steps[*].type` is `merge` or `checkout` or `merge`. - -Users are expected to manage their own branches themselves. - -Type: `bool` - -#### `initialization.steps[*].commit-hash` - -Commit hash. Only read if `initialization.steps[*].type` is `checkout`. - -Type: `string` - -#### `initialization.steps[*].start-point` - -Starting point for creating a new branch. Only read if -`initialization.steps[*].type` is `checkout`. - -When provided, `branch-name` must also be specified and the branch must not -already exist. This creates a new branch at the specified commit reference -(equivalent to `git checkout -b `). - -Accepts any valid git revision: commit SHAs, relative references (e.g., -`HEAD~1`), branch names, or tags. - -Type: `string` - -#### `initialization.steps[*].revision` - -Git reference (commit, branch, tag, or relative ref like `HEAD~1`) to reset or revert to. -Only read if `initialization.steps[*].type` is `reset` or `revert`. Required. - -Type: `string` - -#### `initialization.steps[*].remote-name` - -Remote name. Only read if `initialization.steps[*].type` is `remote` or `fetch`. - -Type: `string` - -#### `initialization.steps[*].remote-url` - -Remote URL. Only read if `initialization.steps[*].type` is `remote`. - -Type: `string` - -#### `initialization.steps[*].mode` - -Reset mode. Only read if `initialization.steps[*].type` is `reset`. Required. - -Accepted values: `soft`, `mixed`, `hard` - -Type: `string` - - -## Lifecycle hooks - -When a step `initializations.steps[*].id` is declared, the step can be -identified with a unique step `id` ([discussed here](#initializationstepsid)) -and lifecycle hooks will be automatically installed for the step. - -Lifecycle hooks are useful when the unit test wants to provide some guarantees -about the Git repository as it is being built (for instance, ensuring that a -commit is present). - -There are two primary lifecycle hooks that the unit test can have access to -during the initialization process: - -1. Pre-hook: run right before the step is run -2. Post-hook: run right after the step has completed - -Take the following file: - -```yaml -# File: hooks.yml -name: Testing hooks -description: | - Lifecycle hooks on each step give visibility into the initialization process -initialization: - steps: - - name: First commit - type: commit - message: First commit - empty: true - id: first-commit - - name: Creating a new file - type: new-file - filename: test.txt - contents: | - Hello world! - - name: Adding test.txt - type: add - files: - - test.txt - id: add-test-txt - - name: Second commit - type: commit - message: Add test.txt -``` - -The overall lifecycle of the above initialization would be: - -1. Propagate `first-commit::pre-hook` event -2. Execute "First commit" -3. Propagate `first-commit::post-hook` event -4. Execute "Creating a new file" -5. Propagate `add-test-text::pre-hook` event -6. Execute "Adding test.txt" -7. Propagate `add-test-text::post-hook` event -8. Execute "Second commit" - -In the unit test, the hooks can be declared as such: - -```python -from repo_smith import initialize_repo - -def test_hook() -> None: - def first_commit_pre_hook(r: Repo) -> None: - print(r) - - spec_path = "hooks.yml" - repo_initializer = initialize_repo(spec_path) - repo_initializer.add_pre_hook("first-commit", first_commit_pre_hook) - with repo_initializer.initialize() as repo: - print(repo.repo) -``` - -## FAQ - -### What if I want to attach a tag based on metadata of the commit history? - -As `repo-smith` is designed to be as generic as possible, it does not have -strong ties with how `git-autograder` works even though both are designed to -work for Git Mastery. - -To do so, you can use a `post-hook` on the commit that should receive the tag: - -```yml -# dynamic-tag.yml -name: Dynamic tag hook -description: Dynamically attaching a tag based on commit history data -initialization: - steps: - - name: Create file - type: new-file - filename: filea.txt - contents: | - Hello world! - - name: Add filea.txt - type: add - files: - - filea.txt - - name: Initial commit - type: commit - message: Committing file - id: initial-commit -``` - -```py -from repo_smith import initialize_repo - -def test_hook() -> None: - def tag_commit_post_hook(r: Repo) -> None: - first_commit = list(r.iter_commits("main", max_count=1))[0] - hexsha = first_commit.hexsha[:7] - r.create_tag(f"git-mastery-{hexsha}") - - spec_path = "dynamic-tag.yml" - repo_initializer = initialize_repo(spec_path) - repo_initializer.add_post_hook("initial-commit", first_commit_post_hook) - with repo_initializer.initialize() as repo: - print(repo.repo) -``` - -In this design, we are able to very quickly attach tags to various commits -without much workarounds. diff --git a/src/repo_smith/clone_from.py b/src/repo_smith/clone_from.py deleted file mode 100644 index 1273e1e..0000000 --- a/src/repo_smith/clone_from.py +++ /dev/null @@ -1,10 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class CloneFrom: - """Indicates that the first step of the initialization should be to clone the - indicated repository and then apply commits to it. - """ - - repo_url: str diff --git a/src/repo_smith/initialize_repo.py b/src/repo_smith/initialize_repo.py deleted file mode 100644 index 4092bea..0000000 --- a/src/repo_smith/initialize_repo.py +++ /dev/null @@ -1,136 +0,0 @@ -import os -import shutil -import tempfile -from contextlib import contextmanager -from typing import Any, Callable, Dict, Iterator, Optional, Set, TypeAlias - -import yaml -from git import Repo - -import repo_smith.steps.tag_step -from repo_smith.clone_from import CloneFrom -from repo_smith.spec import Spec -from repo_smith.steps.dispatcher import Dispatcher - -Hook: TypeAlias = Callable[[Repo], None] - - -class RepoInitializer: - def __init__(self, spec_data: Any) -> None: - self.__spec_data = spec_data - self.__pre_hooks: Dict[str, Hook] = {} - self.__post_hooks: Dict[str, Hook] = {} - - self.__spec = self.__parse_spec(self.__spec_data) - self.__validate_spec(self.__spec) - self.__step_ids = self.__get_all_ids(self.__spec) - - @contextmanager - def initialize(self, existing_path: Optional[str] = None) -> Iterator[Repo]: - tmp_dir = tempfile.mkdtemp() if existing_path is None else existing_path - repo: Optional[Repo] = None - try: - if self.__spec.clone_from is not None: - repo = Repo.clone_from(self.__spec.clone_from.repo_url, tmp_dir) - else: - repo = Repo.init(tmp_dir, initial_branch="main") - - for step in self.__spec.steps: - if step.id in self.__pre_hooks: - self.__pre_hooks[step.id](repo) - - step.execute(repo=repo) - - if step.id in self.__post_hooks: - self.__post_hooks[step.id](repo) - yield repo - finally: - if repo is not None: - repo.git.clear_cache() - shutil.rmtree(tmp_dir) - - def add_pre_hook(self, id: str, hook: Hook) -> None: - if id not in self.__step_ids: - ids = "\n".join([f"- {id}" for id in self.__step_ids]) - raise ValueError( - f"ID {id} not found in spec's steps. Available IDs:\n{ids}" - ) - - if id in self.__pre_hooks: - raise ValueError( - f"ID {id} already has a pre-hook set. Did you mean to add a post_hook instead?" - ) - - self.__pre_hooks[id] = hook - - def add_post_hook(self, id: str, hook: Hook) -> None: - if id not in self.__step_ids: - ids = "\n".join([f"- {id}" for id in self.__step_ids]) - raise ValueError( - f"ID {id} not found in spec's steps. Available IDs:\n{ids}" - ) - - if id in self.__post_hooks: - raise ValueError( - f"ID {id} already has a post-hook set. Did you mean to add a pre_hook instead?" - ) - - self.__post_hooks[id] = hook - - def __validate_spec(self, spec: Spec) -> None: - ids: Set[str] = set() - tags: Set[str] = set() - for step in spec.steps: - if step.id is not None: - if step.id in ids: - raise ValueError( - f"ID {step.id} is duplicated from a previous step. All IDs should be unique." - ) - ids.add(step.id) - - if isinstance(step, repo_smith.steps.tag_step.TagStep): - if step.tag_name in tags: - raise ValueError( - f"Tag {step.tag_name} is already in use by a previous step. All tag names should be unique." - ) - tags.add(step.tag_name) - - def __get_all_ids(self, spec: Spec) -> Set[str]: - ids = set() - for step in spec.steps: - if step.id is not None: - ids.add(step.id) - return ids - - def __parse_spec(self, spec: Any) -> Spec: - steps = [] - - for step in spec.get("initialization", {}).get("steps", []) or []: - steps.append(Dispatcher.dispatch(step)) - - clone_from = None - if spec.get("initialization", {}).get("clone-from", None) is not None: - clone_from = CloneFrom( - repo_url=spec.get("initialization", {}).get("clone-from", "") - ) - - return Spec( - name=spec.get("name", "") or "", - description=spec.get("description", "") or "", - steps=steps, - clone_from=clone_from, - ) - - -def initialize_repo(spec_path: str) -> RepoInitializer: - if not os.path.isfile(spec_path): - raise ValueError("Invalid spec_path provided, not found.") - - with open(spec_path, "rb") as spec_file: - try: - spec_data = yaml.safe_load(spec_file) - if spec_data is None: - raise ValueError("Incomplete spec file.") - return RepoInitializer(spec_data) - except Exception as e: - raise e diff --git a/src/repo_smith/spec.py b/src/repo_smith/spec.py deleted file mode 100644 index 0479d16..0000000 --- a/src/repo_smith/spec.py +++ /dev/null @@ -1,13 +0,0 @@ -from dataclasses import dataclass -from typing import List, Optional - -from repo_smith.clone_from import CloneFrom -from repo_smith.steps.step import Step - - -@dataclass -class Spec: - name: str - description: Optional[str] - steps: List[Step] - clone_from: Optional[CloneFrom] diff --git a/src/repo_smith/steps/add_step.py b/src/repo_smith/steps/add_step.py deleted file mode 100644 index 51781fd..0000000 --- a/src/repo_smith/steps/add_step.py +++ /dev/null @@ -1,37 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, List, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class AddStep(Step): - files: List[str] - - step_type: StepType = field(init=False, default=StepType.ADD) - - def execute(self, repo: Repo) -> None: - repo.index.add(self.files) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "files" not in step: - raise ValueError('Missing "files" field in add step.') - - if step["files"] is None or step["files"] == []: - raise ValueError('Empty "files" list in add step.') - - return cls( - name=name, - description=description, - id=id, - files=step["files"], - ) diff --git a/src/repo_smith/steps/bash_step.py b/src/repo_smith/steps/bash_step.py deleted file mode 100644 index 4e0591a..0000000 --- a/src/repo_smith/steps/bash_step.py +++ /dev/null @@ -1,40 +0,0 @@ -import subprocess -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class BashStep(Step): - body: str - - step_type: StepType = field(init=False, default=StepType.BASH) - - def execute(self, repo: Repo) -> None: - subprocess.check_call( - self.body.strip(), shell=True, executable="/bin/bash", cwd=repo.working_dir - ) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "runs" not in step: - raise ValueError('Missing "runs" field in bash step.') - - if step["runs"] is None or step["runs"].strip() == "": - raise ValueError('Empty "runs" field in bash step.') - - return cls( - name=name, - description=description, - id=id, - body=step["runs"], - ) diff --git a/src/repo_smith/steps/branch_delete_step.py b/src/repo_smith/steps/branch_delete_step.py deleted file mode 100644 index 27eb70b..0000000 --- a/src/repo_smith/steps/branch_delete_step.py +++ /dev/null @@ -1,42 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class BranchDeleteStep(Step): - branch_name: str - - step_type: StepType = field(init=False, default=StepType.BRANCH_DELETE) - - def execute(self, repo: Repo) -> None: - current_local_refs = [ref.name for ref in repo.refs] - if self.branch_name not in current_local_refs: - raise ValueError( - '"branch-name" field provided does not correspond to any existing branches in branch-delete step.' - ) - repo.delete_head(self.branch_name, force=True) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "branch-name" not in step: - raise ValueError('Missing "branch-name" field in branch-delete step.') - - if step["branch-name"] is None or step["branch-name"].strip() == "": - raise ValueError('Empty "branch-name" field in branch-delete step.') - - return cls( - name=name, - description=description, - id=id, - branch_name=step["branch-name"], - ) diff --git a/src/repo_smith/steps/branch_rename_step.py b/src/repo_smith/steps/branch_rename_step.py deleted file mode 100644 index 621ea41..0000000 --- a/src/repo_smith/steps/branch_rename_step.py +++ /dev/null @@ -1,54 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class BranchRenameStep(Step): - original_branch_name: str - target_branch_name: str - - step_type: StepType = field(init=False, default=StepType.BRANCH) - - def execute(self, repo: Repo) -> None: - if self.original_branch_name not in repo.heads: - raise ValueError( - '"branch-name" field provided does not correspond to any existing branches in branch-rename step.' - ) - if self.target_branch_name in repo.heads: - raise ValueError( - '"new-name" field provided corresponds to an existing branch already in branch-rename step.' - ) - branch = repo.heads[self.original_branch_name] - branch.rename(self.target_branch_name) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "branch-name" not in step: - raise ValueError('Missing "branch-name" field in branch-rename step.') - - if step["branch-name"] is None or step["branch-name"].strip() == "": - raise ValueError('Empty "branch-name" field in branch-rename step.') - - if "new-name" not in step: - raise ValueError('Missing "new-name" field in branch-rename step.') - - if step["new-name"] is None or step["new-name"].strip() == "": - raise ValueError('Empty "new-name" field in branch-rename step.') - - return cls( - name=name, - description=description, - id=id, - original_branch_name=step["branch-name"], - target_branch_name=step["new-name"], - ) diff --git a/src/repo_smith/steps/branch_step.py b/src/repo_smith/steps/branch_step.py deleted file mode 100644 index 930d606..0000000 --- a/src/repo_smith/steps/branch_step.py +++ /dev/null @@ -1,39 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class BranchStep(Step): - branch_name: str - - step_type: StepType = field(init=False, default=StepType.BRANCH) - - def execute(self, repo: Repo) -> None: - # TODO: Handle when attempting to create a branch when no commits exist - branch = repo.create_head(self.branch_name) - branch.checkout() - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "branch-name" not in step: - raise ValueError('Missing "branch-name" field in branch step.') - - if step["branch-name"] is None or step["branch-name"].strip() == "": - raise ValueError('Empty "branch-name" field in branch step.') - - return cls( - name=name, - description=description, - id=id, - branch_name=step["branch-name"], - ) diff --git a/src/repo_smith/steps/checkout_step.py b/src/repo_smith/steps/checkout_step.py deleted file mode 100644 index d3d09ec..0000000 --- a/src/repo_smith/steps/checkout_step.py +++ /dev/null @@ -1,76 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import BadName, Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class CheckoutStep(Step): - branch_name: Optional[str] - commit_hash: Optional[str] - start_point: Optional[str] - - step_type: StepType = field(init=False, default=StepType.CHECKOUT) - - def execute(self, repo: Repo) -> None: - if self.branch_name is not None: - if self.start_point is not None: - if self.branch_name in repo.heads: - raise ValueError( - f'Branch "{self.branch_name}" already exists. Cannot use "start-point" with an existing branch in checkout step.' - ) - repo.git.checkout("-b", self.branch_name, self.start_point) - elif self.branch_name not in repo.heads: - raise ValueError("Invalid branch name") - else: - repo.heads[self.branch_name].checkout() - - if self.commit_hash: - try: - commit = repo.commit(self.commit_hash) - repo.git.checkout(commit) - except (ValueError, BadName): - raise ValueError("Commit not found") - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if step.get("branch-name") is None and step.get("commit-hash") is None: - raise ValueError( - 'Provide either "branch-name" or "commit-hash" in checkout step.' - ) - - if step.get("branch-name") is not None and step.get("commit-hash") is not None: - raise ValueError( - 'Provide either "branch-name" or "commit-hash", not both, in checkout step.' - ) - - if step.get("branch-name") is not None and step["branch-name"].strip() == "": - raise ValueError('Empty "branch-name" field in checkout step.') - - if step.get("branch-name") is None and step.get("start-point") is not None: - raise ValueError( - '"start-point" field requires "branch-name" field to be provided in checkout step.' - ) - - if step.get("commit-hash") is not None and step["commit-hash"].strip() == "": - raise ValueError('Empty "commit-hash" field in checkout step.') - - if step.get("start-point") is not None and step["start-point"].strip() == "": - raise ValueError('Empty "start-point" field in checkout step.') - - return cls( - name=name, - description=description, - id=id, - branch_name=step.get("branch-name"), - commit_hash=step.get("commit-hash"), - start_point=step.get("start-point"), - ) diff --git a/src/repo_smith/steps/commit_step.py b/src/repo_smith/steps/commit_step.py deleted file mode 100644 index fb5e003..0000000 --- a/src/repo_smith/steps/commit_step.py +++ /dev/null @@ -1,42 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class CommitStep(Step): - empty: bool - message: str - - step_type: StepType = field(init=False, default=StepType.COMMIT) - - def execute(self, repo: Repo) -> None: - if self.empty: - repo.git.commit("-m", self.message, "--allow-empty") - else: - repo.index.commit(message=self.message) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "message" not in step: - raise ValueError('Missing "message" field in commit step.') - - if step["message"] is None or step["message"].strip() == "": - raise ValueError('Empty "message" field in commit step.') - - return cls( - name=name, - description=description, - id=id, - empty=step.get("empty", False), - message=step["message"], - ) diff --git a/src/repo_smith/steps/dispatcher.py b/src/repo_smith/steps/dispatcher.py deleted file mode 100644 index 95ae98e..0000000 --- a/src/repo_smith/steps/dispatcher.py +++ /dev/null @@ -1,75 +0,0 @@ -from typing import Any, Type - -from repo_smith.steps.add_step import AddStep -from repo_smith.steps.bash_step import BashStep -from repo_smith.steps.branch_delete_step import BranchDeleteStep -from repo_smith.steps.branch_rename_step import BranchRenameStep -from repo_smith.steps.branch_step import BranchStep -from repo_smith.steps.checkout_step import CheckoutStep -from repo_smith.steps.commit_step import CommitStep -from repo_smith.steps.fetch_step import FetchStep -from repo_smith.steps.file_step import ( - AppendFileStep, - DeleteFileStep, - EditFileStep, - NewFileStep, -) -from repo_smith.steps.merge_step import MergeStep -from repo_smith.steps.remote_step import RemoteStep -from repo_smith.steps.reset_step import ResetStep -from repo_smith.steps.revert_step import RevertStep -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType -from repo_smith.steps.tag_step import TagStep - - -class Dispatcher: - @staticmethod - def dispatch(step: Any) -> Step: - if "type" not in step: - raise ValueError('Missing "type" field in step.') - - name = step.get("name") - description = step.get("description") - step_type = StepType.from_value(step["type"]) - id = step.get("id") - retrieved_step_type = Dispatcher.__get_type(step_type) - return retrieved_step_type.parse(name, description, id, step) - - @staticmethod - def __get_type(step_type: StepType) -> Type[Step]: - match step_type: - case StepType.COMMIT: - return CommitStep - case StepType.ADD: - return AddStep - case StepType.TAG: - return TagStep - case StepType.BASH: - return BashStep - case StepType.BRANCH: - return BranchStep - case StepType.BRANCH_RENAME: - return BranchRenameStep - case StepType.BRANCH_DELETE: - return BranchDeleteStep - case StepType.CHECKOUT: - return CheckoutStep - case StepType.MERGE: - return MergeStep - case StepType.REMOTE: - return RemoteStep - case StepType.RESET: - return ResetStep - case StepType.REVERT: - return RevertStep - case StepType.FETCH: - return FetchStep - case StepType.NEW_FILE: - return NewFileStep - case StepType.EDIT_FILE: - return EditFileStep - case StepType.DELETE_FILE: - return DeleteFileStep - case StepType.APPEND_FILE: - return AppendFileStep diff --git a/src/repo_smith/steps/fetch_step.py b/src/repo_smith/steps/fetch_step.py deleted file mode 100644 index 21d535c..0000000 --- a/src/repo_smith/steps/fetch_step.py +++ /dev/null @@ -1,42 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class FetchStep(Step): - remote_name: str - - step_type: StepType = field(init=False, default=StepType.FETCH) - - def execute(self, repo: Repo) -> None: - try: - remote = repo.remote(self.remote_name) - except Exception: - raise ValueError(f"Missing remote '{self.remote_name}' in fetch step.") - - remote.fetch() - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "remote-name" not in step: - raise ValueError('Missing "remote-name" field in fetch step.') - - if step["remote-name"] is None or step["remote-name"].strip() == "": - raise ValueError('Empty "remote-name" field in fetch step.') - - return cls( - name=name, - description=description, - id=id, - remote_name=step["remote-name"], - ) diff --git a/src/repo_smith/steps/file_step.py b/src/repo_smith/steps/file_step.py deleted file mode 100644 index e1b6377..0000000 --- a/src/repo_smith/steps/file_step.py +++ /dev/null @@ -1,146 +0,0 @@ -import os -import os.path -import pathlib -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Tuple, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class FileStep(Step): - filename: str - contents: str - - @staticmethod - def get_details(step: Any) -> Tuple[str, str]: - if "filename" not in step: - raise ValueError('Missing "filename" field in file step.') - - if step["filename"] is None or step["filename"].strip() == "": - raise ValueError('Empty "filename" field in file step.') - - filename = step["filename"] - contents = step.get("contents", "") or "" - return filename, contents - - -@dataclass -class NewFileStep(FileStep): - step_type: StepType = field(init=False, default=StepType.NEW_FILE) - - def execute(self, repo: Repo) -> None: - rw_dir = repo.working_dir - filepath = os.path.join(rw_dir, self.filename) - filepath_dir_only = os.path.dirname(filepath) - pathlib.Path(filepath_dir_only).mkdir(parents=True, exist_ok=True) - with open(filepath, "w+") as fs: - fs.write(self.contents) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - filename, contents = FileStep.get_details(step) - return cls( - name=name, - description=description, - id=id, - filename=filename, - contents=contents, - ) - - -@dataclass -class EditFileStep(FileStep): - step_type: StepType = field(init=False, default=StepType.EDIT_FILE) - - def execute(self, repo: Repo) -> None: - rw_dir = repo.working_dir - filepath = os.path.join(rw_dir, self.filename) - if not os.path.isfile(filepath): - raise ValueError("Invalid filename for editing") - with open(filepath, "w") as fs: - fs.write(self.contents) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - filename, contents = FileStep.get_details(step) - return cls( - name=name, - description=description, - id=id, - filename=filename, - contents=contents, - ) - - -@dataclass -class DeleteFileStep(FileStep): - step_type: StepType = field(init=False, default=StepType.DELETE_FILE) - - def execute(self, repo: Repo) -> None: - rw_dir = repo.working_dir - filepath = os.path.join(rw_dir, self.filename) - if not os.path.isfile(filepath): - raise ValueError("Invalid filename for deleting") - os.remove(filepath) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - filename, contents = FileStep.get_details(step) - return cls( - name=name, - description=description, - id=id, - filename=filename, - contents=contents, - ) - - -@dataclass -class AppendFileStep(FileStep): - step_type: StepType = field(init=False, default=StepType.APPEND_FILE) - - def execute(self, repo: Repo) -> None: - rw_dir = repo.working_dir - filepath = os.path.join(rw_dir, self.filename) - if not os.path.isfile(filepath): - raise ValueError("Invalid filename for appending") - with open(filepath, "a") as fs: - fs.write(self.contents) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - filename, contents = FileStep.get_details(step) - return cls( - name=name, - description=description, - id=id, - filename=filename, - contents=contents, - ) diff --git a/src/repo_smith/steps/merge_step.py b/src/repo_smith/steps/merge_step.py deleted file mode 100644 index a1976bc..0000000 --- a/src/repo_smith/steps/merge_step.py +++ /dev/null @@ -1,52 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class MergeStep(Step): - branch_name: str - no_fast_forward: bool - squash: bool - - step_type: StepType = field(init=False, default=StepType.MERGE) - - def execute(self, repo: Repo) -> None: - # TODO: Maybe handle merge conflicts as they happen - merge_args = [self.branch_name, "--no-edit"] - - if self.squash: - merge_args.append("--squash") - elif self.no_fast_forward: - merge_args.append("--no-ff") - - repo.git.merge(*merge_args) - - if self.squash: - repo.git.commit("-m", f"Squash merge branch '{self.branch_name}'") - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "branch-name" not in step: - raise ValueError('Missing "branch-name" field in merge step.') - - if step["branch-name"] is None or step["branch-name"].strip() == "": - raise ValueError('Empty "branch-name" field in merge step.') - - return cls( - name=name, - description=description, - id=id, - branch_name=step.get("branch-name"), - no_fast_forward=step.get("no-ff", False), - squash=step.get("squash", False), - ) diff --git a/src/repo_smith/steps/remote_step.py b/src/repo_smith/steps/remote_step.py deleted file mode 100644 index a6e8a38..0000000 --- a/src/repo_smith/steps/remote_step.py +++ /dev/null @@ -1,45 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class RemoteStep(Step): - remote_name: str - remote_url: str - - step_type: StepType = field(init=False, default=StepType.REMOTE) - - def execute(self, repo: Repo) -> None: - repo.create_remote(self.remote_name, self.remote_url) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "remote-url" not in step: - raise ValueError('Missing "remote-url" field in remote step.') - - if step["remote-url"] is None or step["remote-url"].strip() == "": - raise ValueError('Empty "remote-url" field in remote step.') - - if "remote-name" not in step: - raise ValueError('Missing "remote-name" field in remote step.') - - if step["remote-name"] is None or step["remote-name"].strip() == "": - raise ValueError('Empty "remote-name" field in remote step.') - - return cls( - name=name, - description=description, - id=id, - remote_name=step["remote-name"], - remote_url=step["remote-url"], - ) diff --git a/src/repo_smith/steps/reset_step.py b/src/repo_smith/steps/reset_step.py deleted file mode 100644 index dda8e10..0000000 --- a/src/repo_smith/steps/reset_step.py +++ /dev/null @@ -1,61 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, List, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - -VALID_MODES = ("soft", "mixed", "hard") - - -@dataclass -class ResetStep(Step): - revision: Optional[str] - mode: str - files: Optional[List[str]] - - step_type: StepType = field(init=False, default=StepType.RESET) - - def execute(self, repo: Repo) -> None: - if self.files: - repo.git.reset(self.revision, "--", *self.files) - else: - repo.git.reset(f"--{self.mode}", self.revision) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "mode" not in step: - raise ValueError('Missing "mode" field in reset step.') - - if step["mode"] is None or step["mode"].strip().lower() not in VALID_MODES: - raise ValueError(f'Invalid "mode" value. Must be one of: {VALID_MODES}.') - - if "revision" not in step: - raise ValueError('Missing "revision" field in reset step.') - - if step["revision"] is None or step["revision"].strip() == "": - raise ValueError('Empty "revision" field in reset step.') - - if "files" in step and step["files"] is not None: - if step["files"] == []: - raise ValueError('Empty "files" list in reset step.') - - if step["mode"] != "mixed": - raise ValueError( - f'Cannot use "files" field with "{step["mode"]}" mode in reset step. Only "mixed" mode is allowed with files.' - ) - - return cls( - name=name, - description=description, - id=id, - revision=step["revision"], - mode=step["mode"], - files=step.get("files", None), - ) diff --git a/src/repo_smith/steps/revert_step.py b/src/repo_smith/steps/revert_step.py deleted file mode 100644 index 054a20c..0000000 --- a/src/repo_smith/steps/revert_step.py +++ /dev/null @@ -1,39 +0,0 @@ -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class RevertStep(Step): - revision: str - - step_type: StepType = field(init=False, default=StepType.REVERT) - - def execute(self, repo: Repo) -> None: - revert_args = [self.revision, "--no-edit"] - - repo.git.revert(*revert_args) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "revision" not in step: - raise ValueError('Missing "revision" field in revert step.') - - if step["revision"] is None or step["revision"].strip() == "": - raise ValueError('Empty "revision" field in revert step.') - - return cls( - name=name, - description=description, - id=id, - revision=step["revision"], - ) diff --git a/src/repo_smith/steps/step.py b/src/repo_smith/steps/step.py deleted file mode 100644 index 7f17b5c..0000000 --- a/src/repo_smith/steps/step.py +++ /dev/null @@ -1,29 +0,0 @@ -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step_type import StepType - - -@dataclass -class Step(ABC): - name: Optional[str] - step_type: StepType - description: Optional[str] - id: Optional[str] - - @abstractmethod - def execute(self, repo: Repo) -> None: - pass - - @classmethod - @abstractmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - pass diff --git a/src/repo_smith/steps/step_type.py b/src/repo_smith/steps/step_type.py deleted file mode 100644 index 3aaa3f4..0000000 --- a/src/repo_smith/steps/step_type.py +++ /dev/null @@ -1,61 +0,0 @@ -from enum import Enum - - -class StepType(Enum): - COMMIT = "commit" - ADD = "add" - TAG = "tag" - NEW_FILE = "new-file" - EDIT_FILE = "edit-file" - DELETE_FILE = "delete-file" - APPEND_FILE = "append-file" - BASH = "bash" - BRANCH = "branch" - BRANCH_RENAME = "branch-rename" - BRANCH_DELETE = "branch-delete" - CHECKOUT = "checkout" - REMOTE = "remote" - RESET = "reset" - REVERT = "revert" - MERGE = "merge" - FETCH = "fetch" - - @staticmethod - def from_value(value: str) -> "StepType": - match value: - case "commit": - return StepType.COMMIT - case "add": - return StepType.ADD - case "tag": - return StepType.TAG - case "new-file": - return StepType.NEW_FILE - case "edit-file": - return StepType.EDIT_FILE - case "delete-file": - return StepType.DELETE_FILE - case "append-file": - return StepType.APPEND_FILE - case "bash": - return StepType.BASH - case "branch": - return StepType.BRANCH - case "branch-rename": - return StepType.BRANCH_RENAME - case "branch-delete": - return StepType.BRANCH_DELETE - case "checkout": - return StepType.CHECKOUT - case "remote": - return StepType.REMOTE - case "reset": - return StepType.RESET - case "revert": - return StepType.REVERT - case "merge": - return StepType.MERGE - case "fetch": - return StepType.FETCH - case _: - raise ValueError(f"Invalid value {value} given. Not supported.") diff --git a/src/repo_smith/steps/tag_step.py b/src/repo_smith/steps/tag_step.py deleted file mode 100644 index 6014934..0000000 --- a/src/repo_smith/steps/tag_step.py +++ /dev/null @@ -1,46 +0,0 @@ -import re -from dataclasses import dataclass, field -from typing import Any, Optional, Self, Type - -from git import Repo -from repo_smith.steps.step import Step -from repo_smith.steps.step_type import StepType - - -@dataclass -class TagStep(Step): - tag_name: str - tag_message: Optional[str] - - step_type: StepType = field(init=False, default=StepType.TAG) - - def execute(self, repo: Repo) -> None: - repo.create_tag(self.tag_name, message=self.tag_message) - - @classmethod - def parse( - cls: Type[Self], - name: Optional[str], - description: Optional[str], - id: Optional[str], - step: Any, - ) -> Self: - if "tag-name" not in step: - raise ValueError('Missing "tag-name" field in tag step.') - - if step["tag-name"] is None or step["tag-name"].strip() == "": - raise ValueError('Empty "tag-name" field in tag step.') - - tag_name_regex = "^[0-9a-zA-Z-_.]*$" - if re.search(tag_name_regex, step["tag-name"]) is None: - raise ValueError( - 'Field "tag-name" can only contain alphanumeric characters, _, -, .' - ) - - return cls( - name=name, - description=description, - id=id, - tag_name=step["tag-name"], - tag_message=step.get("tag-message"), - ) diff --git a/tests/integration/steps/test_add_step.py b/tests/integration/steps/test_add_step.py deleted file mode 100644 index 3358023..0000000 --- a/tests/integration/steps/test_add_step.py +++ /dev/null @@ -1,29 +0,0 @@ -import os - -from git import Repo -import pytest - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_add_step_missing_files(): - with pytest.raises(ValueError, match='Missing "files" field in add step.'): - initialize_repo("tests/specs/add_step/add_step_missing_files.yml") - - -def test_add_step_empty_files(): - with pytest.raises(ValueError, match='Empty "files" list in add step.'): - initialize_repo("tests/specs/add_step/add_step_empty_files.yml") - - -def test_add_step(): - def pre_hook(r: Repo) -> None: - filename = os.path.join(r.working_dir, "file.txt") - assert os.path.isfile(filename) - - repo_initializer = initialize_repo("tests/specs/add_step/add_step.yml") - repo_initializer.add_pre_hook("add", pre_hook) - - with repo_initializer.initialize() as r: - assert len(r.index.entries) == 1 - assert ("file.txt", 0) in r.index.entries diff --git a/tests/integration/steps/test_append_file_step.py b/tests/integration/steps/test_append_file_step.py deleted file mode 100644 index bb1501c..0000000 --- a/tests/integration/steps/test_append_file_step.py +++ /dev/null @@ -1,44 +0,0 @@ -import os - -import pytest -from git import Repo - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_append_file_step_missing_filename(): - with pytest.raises(Exception): - initialize_repo("tests/specs/append_file_step/append_file_missing_filename.yml") - - -def test_append_file_step_no_filename(): - with pytest.raises(Exception): - initialize_repo("tests/specs/append_file_step/append_file_no_filename.yml") - - -def test_append_file_step(): - def pre_hook(r: Repo) -> None: - filepath = os.path.join(r.working_dir, "file.txt") - assert os.path.isfile(filepath) - expected = ["Hello world"] - with open(filepath, "r") as f: - lines = [line.strip() for line in f.readlines()] - assert len(lines) == len(expected) - for actual, ex in zip(lines, expected): - assert actual == ex - - def post_hook(r: Repo) -> None: - filepath = os.path.join(r.working_dir, "file.txt") - assert os.path.isfile(filepath) - expected = ["Hello world", "This is a new line!"] - with open(filepath, "r") as f: - lines = [line.strip() for line in f.readlines()] - assert len(lines) == len(expected) - for actual, ex in zip(lines, expected): - assert actual == ex - - repo_initializer = initialize_repo("tests/specs/append_file_step/append_file.yml") - repo_initializer.add_pre_hook("append-file", pre_hook) - repo_initializer.add_post_hook("append-file", post_hook) - with repo_initializer.initialize(): - pass diff --git a/tests/integration/steps/test_bash_step.py b/tests/integration/steps/test_bash_step.py deleted file mode 100644 index f462b53..0000000 --- a/tests/integration/steps/test_bash_step.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import subprocess - -import pytest - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_bash_step_missing_runs(): - with pytest.raises(ValueError, match='Missing "runs" field in bash step.'): - initialize_repo("tests/specs/bash_step/bash_step_missing_runs.yml") - - -@pytest.mark.skip( - reason="an actual empty field is not parsed so we can safely ignore this" -) -def test_bash_step_empty_runs(): - with pytest.raises(ValueError, match='Empty "runs" field in bash step.'): - initialize_repo("tests/specs/bash_step/bash_step_missing_runs.yml") - - -def test_bash_step_invalid_runs(): - with pytest.raises(subprocess.CalledProcessError): - repo_initializer = initialize_repo( - "tests/specs/bash_step/bash_step_invalid_runs.yml" - ) - with repo_initializer.initialize(): - pass - - -def test_bash_step(): - repo_initializer = initialize_repo("tests/specs/bash_step/bash_step.yml") - with repo_initializer.initialize() as r: - dirs = os.listdir(r.working_dir) - assert ( - len( - set(dirs) - & {"file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"} - ) - == 5 - ) - with open(os.path.join(r.working_dir, "file4.txt"), "r") as file: - assert file.readlines() == ["Hello world\n"] diff --git a/tests/integration/steps/test_branch_delete_step.py b/tests/integration/steps/test_branch_delete_step.py deleted file mode 100644 index ef01387..0000000 --- a/tests/integration/steps/test_branch_delete_step.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest -from repo_smith.initialize_repo import initialize_repo - - -def test_branch_delete_step_branch_exists(): - repo_initializer = initialize_repo( - "tests/specs/branch_delete_step/branch_delete_step_branch_exists.yml" - ) - with repo_initializer.initialize() as r: - assert [r.name for r in r.refs] == ["main"] - - -def test_branch_delete_step_branch_does_not_exist(): - repo_initializer = initialize_repo( - "tests/specs/branch_delete_step/branch_delete_step_branch_does_not_exist.yml" - ) - - with pytest.raises( - ValueError, - match='"branch-name" field provided does not correspond to any existing branches in branch-delete step.', - ): - with repo_initializer.initialize() as _: - pass diff --git a/tests/integration/steps/test_branch_rename_step.py b/tests/integration/steps/test_branch_rename_step.py deleted file mode 100644 index 90a9e40..0000000 --- a/tests/integration/steps/test_branch_rename_step.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest -from repo_smith.initialize_repo import initialize_repo - - -def test_branch_rename_step_branch_exists(): - repo_initializer = initialize_repo( - "tests/specs/branch_rename_step/branch_rename_step_branch_exists.yml" - ) - with repo_initializer.initialize() as r: - assert {r.name for r in r.refs} == {"main", "primary"} - - -def test_branch_rename_step_branch_does_not_exist(): - repo_initializer = initialize_repo( - "tests/specs/branch_rename_step/branch_rename_step_branch_does_not_exist.yml" - ) - with pytest.raises( - ValueError, - match='"branch-name" field provided does not correspond to any existing branches in branch-rename step.', - ): - with repo_initializer.initialize() as _: - pass - - -def test_branch_rename_step_branch_already_existed(): - repo_initializer = initialize_repo( - "tests/specs/branch_rename_step/branch_rename_step_branch_already_existed.yml" - ) - with pytest.raises( - ValueError, - match='"new-name" field provided corresponds to an existing branch already in branch-rename step.', - ): - with repo_initializer.initialize() as _: - pass diff --git a/tests/integration/steps/test_branch_step.py b/tests/integration/steps/test_branch_step.py deleted file mode 100644 index cf2b24f..0000000 --- a/tests/integration/steps/test_branch_step.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -from git import Repo -import pytest - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_branch_step_missing_branch_name(): - with pytest.raises(Exception): - initialize_repo("tests/specs/branch_step/branch_step_missing_branch_name.yml") - - -def test_branch_step_empty_branch_name(): - with pytest.raises(Exception): - initialize_repo("tests/specs/branch_step/branch_step_empty_branch_name.yml") - - -def test_branch_step(): - repo_initializer = initialize_repo("tests/specs/branch_step/branch_step.yml") - with repo_initializer.initialize() as r: - assert len(r.branches) == 2 - assert "test" in r.heads diff --git a/tests/integration/steps/test_checkout_step.py b/tests/integration/steps/test_checkout_step.py deleted file mode 100644 index f502c31..0000000 --- a/tests/integration/steps/test_checkout_step.py +++ /dev/null @@ -1,74 +0,0 @@ -from git import Repo -import pytest - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_checkout_step_missing_branch_name(): - with pytest.raises(Exception): - initialize_repo( - "tests/specs/checkout_step/checkout_step_missing_branch_name.yml" - ) - - -def test_checkout_step_empty_branch_name(): - with pytest.raises(Exception): - initialize_repo("tests/specs/checkout_step/checkout_step_empty_branch_name.yml") - - -def test_checkout_step_missing_branch(): - with pytest.raises(Exception): - repo_initialier = initialize_repo( - "tests/specs/checkout_step/checkout_step_missing_branch.yml" - ) - with repo_initialier.initialize(): - pass - - -def test_checkout_step_start_point_without_branch(): - with pytest.raises(Exception): - initialize_repo( - "tests/specs/checkout_step/checkout_step_start_point_without_branch.yml" - ) - - -def test_checkout_step_start_point_with_commit_hash(): - with pytest.raises(Exception): - initialize_repo( - "tests/specs/checkout_step/checkout_step_start_point_with_commit_hash.yml" - ) - - -def test_checkout_step_start_point_branch_exists(): - with pytest.raises(Exception): - repo_initializer = initialize_repo( - "tests/specs/checkout_step/checkout_step_start_point_branch_exists.yml" - ) - with repo_initializer.initialize(): - pass - - -def test_checkout_step(): - def first_hook(r: Repo) -> None: - assert r.active_branch.name == "main" - - def second_hook(r: Repo) -> None: - assert r.active_branch.name == "test" - - repo_initializer = initialize_repo("tests/specs/checkout_step/checkout_step.yml") - repo_initializer.add_post_hook("first", first_hook) - repo_initializer.add_post_hook("second", second_hook) - with repo_initializer.initialize() as r: - assert len(r.branches) == 2 - assert "test" in r.heads - - -def test_checkout_step_with_start_point(): - repo_initializer = initialize_repo( - "tests/specs/checkout_step/checkout_step_with_start_point.yml" - ) - with repo_initializer.initialize() as r: - assert r.active_branch.name == "new-branch" - assert len(r.branches) == 2 - assert "new-branch" in r.heads - assert r.heads["new-branch"].commit.message.strip() == "first commit" diff --git a/tests/integration/steps/test_commit_step.py b/tests/integration/steps/test_commit_step.py deleted file mode 100644 index 68bf131..0000000 --- a/tests/integration/steps/test_commit_step.py +++ /dev/null @@ -1,42 +0,0 @@ -from git import Repo -import pytest - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_commit_step_missing_message(): - with pytest.raises(Exception): - initialize_repo("tests/specs/commit_step/commit_step_missing_message.yml") - - -def test_commit_step(): - def pre_hook(r: Repo) -> None: - with pytest.raises(Exception): - # Ensure there is 0 commits - # Must cast to list since the Iterator alone will not raise an error - list(r.iter_commits("main")) - - repo_initializer = initialize_repo("tests/specs/commit_step/commit.yml") - repo_initializer.add_pre_hook("commit", pre_hook) - with repo_initializer.initialize() as r: - commits = list(r.iter_commits("main")) - assert len(commits) == 1 - commit = commits[0] - assert len(commit.stats.files) == 1 - assert "file.txt" in commit.stats.files - - -def test_commit_step_empty_commit(): - def pre_hook(r: Repo) -> None: - with pytest.raises(Exception): - # Ensure there is 0 commits - # Must cast to list since the Iterator alone will not raise an error - list(r.iter_commits("main")) - - repo_initializer = initialize_repo("tests/specs/commit_step/commit_empty.yml") - repo_initializer.add_pre_hook("commit", pre_hook) - with repo_initializer.initialize() as r: - commits = list(r.iter_commits("main")) - assert len(commits) == 1 - commit = commits[0] - assert len(commit.stats.files) == 0 diff --git a/tests/integration/steps/test_delete_file_step.py b/tests/integration/steps/test_delete_file_step.py deleted file mode 100644 index 9cc526d..0000000 --- a/tests/integration/steps/test_delete_file_step.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -import pytest -from git import Repo - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_delete_file_step_missing_filename() -> None: - with pytest.raises(Exception): - initialize_repo("tests/specs/delete_file_step/delete_file_missing_filename.yml") - - -def test_delete_file_step_unknown_file() -> None: - with pytest.raises(Exception): - repo_initializer = initialize_repo( - "tests/specs/delete_file_step/delete_file_no_file.yml" - ) - with repo_initializer.initialize(): - pass - - -def test_delete_file_step() -> None: - def pre_hook(r: Repo) -> None: - filepath = os.path.join(r.working_dir, "file.txt") - assert os.path.isfile(filepath) - - repo_initializer = initialize_repo("tests/specs/delete_file_step/delete_file.yml") - repo_initializer.add_pre_hook("delete-file", pre_hook) - with repo_initializer.initialize() as r: - filepath = os.path.join(r.working_dir, "file.txt") - assert not os.path.isfile(filepath) diff --git a/tests/integration/steps/test_edit_file_step.py b/tests/integration/steps/test_edit_file_step.py deleted file mode 100644 index 5b34f3b..0000000 --- a/tests/integration/steps/test_edit_file_step.py +++ /dev/null @@ -1,45 +0,0 @@ -import os - -import pytest -from git import Repo - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_edit_file_step_unknown_file() -> None: - with pytest.raises(Exception): - repo_initializer = initialize_repo( - "tests/specs/edit_file_step/edit_file_missing.yml" - ) - with repo_initializer.initialize(): - pass - - -def test_edit_file_step() -> None: - def add_hook(r: Repo) -> None: - dir_list = os.listdir(r.working_dir) - assert "filea.txt" in dir_list - filepath = os.path.join(r.working_dir, "filea.txt") - expected_file_contents = ["Original text"] - with open(filepath, "r") as f: - lines = [line.strip() for line in f.readlines()] - assert len(lines) == len(expected_file_contents) - for actual, expected in zip(lines, expected_file_contents): - assert actual == expected - - def edit_hook(r: Repo) -> None: - dir_list = os.listdir(r.working_dir) - assert "filea.txt" in dir_list - filepath = os.path.join(r.working_dir, "filea.txt") - expected_file_contents = ["Edited text"] - with open(filepath, "r") as f: - lines = [line.strip() for line in f.readlines()] - assert len(lines) == len(expected_file_contents) - for actual, expected in zip(lines, expected_file_contents): - assert actual == expected - - repo_initializer = initialize_repo("tests/specs/edit_file_step/edit_file.yml") - repo_initializer.add_post_hook("add", add_hook) - repo_initializer.add_post_hook("edit", edit_hook) - with repo_initializer.initialize(): - pass diff --git a/tests/integration/steps/test_fetch_step.py b/tests/integration/steps/test_fetch_step.py deleted file mode 100644 index acf7ac8..0000000 --- a/tests/integration/steps/test_fetch_step.py +++ /dev/null @@ -1,29 +0,0 @@ -import os - -import pytest -from git import Repo -from repo_smith.initialize_repo import initialize_repo -from tests.fixtures.git_fixtures import REMOTE_REPO_PATH, remote_repo - - -def test_fetch_step_remote_valid(remote_repo: Repo): - (REMOTE_REPO_PATH / "dummy.txt").write_text("initial") - remote_repo.index.add(["dummy.txt"]) - remote_repo.index.commit("initial commit") - - remote_repo_commit_hexsha = remote_repo.commit("main").hexsha - ir = initialize_repo("tests/specs/fetch_step/fetch_step_remote_valid.yml") - with ir.initialize() as r: - latest_commit_hexsha = r.commit("origin/main").hexsha - assert latest_commit_hexsha == remote_repo_commit_hexsha - - -def test_fetch_step_missing_remote(remote_repo: Repo): - (REMOTE_REPO_PATH / "dummy.txt").write_text("initial") - remote_repo.index.add(["dummy.txt"]) - remote_repo.index.commit("initial commit") - - ir = initialize_repo("tests/specs/fetch_step/fetch_step_missing_remote.yml") - with pytest.raises(ValueError, match="Missing remote 'upstream' in fetch step."): - with ir.initialize() as _: - pass diff --git a/tests/integration/steps/test_merge_step.py b/tests/integration/steps/test_merge_step.py deleted file mode 100644 index 2cf4f5e..0000000 --- a/tests/integration/steps/test_merge_step.py +++ /dev/null @@ -1,29 +0,0 @@ -from repo_smith.initialize_repo import initialize_repo - -# TODO: more corner case testing - - -def test_merge_step_squash(): - ir = initialize_repo("tests/specs/merge_step/merge_step_squash.yml") - with ir.initialize() as r: - commits = list(r.iter_commits()) - commit_messages = [c.message.strip() for c in commits][::-1] - assert commit_messages == ["Before", "Squash merge branch 'incoming'", "After"] - - -def test_merge_step_no_fast_forward(): - ir = initialize_repo("tests/specs/merge_step/merge_step_no_fast_forward.yml") - with ir.initialize() as r: - commits = list(r.iter_commits()) - commit_messages = [c.message.strip() for c in commits][::-1] - assert "Merge branch 'incoming'" in commit_messages - assert len(commits[1].parents) == 2 - - -def test_merge_step_with_fast_forward(): - ir = initialize_repo("tests/specs/merge_step/merge_step_with_fast_forward.yml") - with ir.initialize() as r: - commits = list(r.iter_commits()) - commit_messages = [c.message.strip() for c in commits][::-1] - assert "Merge branch 'incoming'" not in commit_messages - assert not any([len(c.parents) > 1 for c in commits]) diff --git a/tests/integration/steps/test_new_file_step.py b/tests/integration/steps/test_new_file_step.py deleted file mode 100644 index 8998d89..0000000 --- a/tests/integration/steps/test_new_file_step.py +++ /dev/null @@ -1,60 +0,0 @@ -import os - -import pytest -from git import Repo - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_new_file_step_missing_filename() -> None: - with pytest.raises(Exception): - initialize_repo("tests/specs/new_file_step/new_file_missing_filename.yml") - - -def test_new_file_step_empty_filename() -> None: - with pytest.raises(Exception): - initialize_repo("tests/specs/new_file_step/new_file_empty_filename.yml") - - -def test_new_file_step_empty_contents() -> None: - repo_initializer = initialize_repo( - "tests/specs/new_file_step/new_file_empty_contents.yml" - ) - with repo_initializer.initialize() as r: - filea = os.path.join(r.working_dir, "filea.txt") - fileb = os.path.join(r.working_dir, "fileb.txt") - assert os.path.isfile(filea) - assert os.path.isfile(fileb) - assert os.path.getsize(filea) == 0 - assert os.path.getsize(fileb) == 0 - - -def test_new_file_step() -> None: - def validate_filea_hook(r: Repo) -> None: - dir_list = os.listdir(r.working_dir) - assert "filea.txt" in dir_list - filepath = os.path.join(r.working_dir, "filea.txt") - expected_file_contents = ["Hello world!", "", "This is a file"] - with open(filepath, "r") as f: - lines = [line.strip() for line in f.readlines()] - assert len(lines) == len(expected_file_contents) - for actual, expected in zip(lines, expected_file_contents): - assert actual == expected - - def validate_nested_file_hook(r: Repo) -> None: - dir_list = os.listdir(r.working_dir) - assert "nested" in dir_list - filepath = os.path.join(r.working_dir, "nested/a/b/c/filed.txt") - assert os.path.isfile(filepath) - expected_file_contents = ["This is a nested file"] - with open(filepath, "r") as f: - lines = [line.strip() for line in f.readlines()] - assert len(lines) == len(expected_file_contents) - for actual, expected in zip(lines, expected_file_contents): - assert actual == expected - - repo_initializer = initialize_repo("tests/specs/new_file_step/new_file.yml") - repo_initializer.add_post_hook("filea", validate_filea_hook) - repo_initializer.add_post_hook("nested_file", validate_nested_file_hook) - with repo_initializer.initialize(): - pass diff --git a/tests/integration/steps/test_remote_step.py b/tests/integration/steps/test_remote_step.py deleted file mode 100644 index efb828f..0000000 --- a/tests/integration/steps/test_remote_step.py +++ /dev/null @@ -1,9 +0,0 @@ -from repo_smith.initialize_repo import initialize_repo - - -def test_remote_step_valid(): - ir = initialize_repo("tests/specs/remote_step/remote_step_valid.yml") - with ir.initialize() as r: - assert len(r.remotes) == 1 - assert r.remotes[0].name == "upstream" - assert r.remotes[0].url == "https://github.com/git-mastery/repo-smith.git" diff --git a/tests/integration/steps/test_reset_step.py b/tests/integration/steps/test_reset_step.py deleted file mode 100644 index a547b80..0000000 --- a/tests/integration/steps/test_reset_step.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -from repo_smith.initialize_repo import initialize_repo - - -def test_reset_step(): - # only test hard reset (other valid modes can be assumed to work if hard works) - repo_initializer = initialize_repo("tests/specs/reset_step/reset_step_hard.yml") - with repo_initializer.initialize() as r: - assert r.head.commit.message.strip() == "Initial commit" - file_path = os.path.join(r.working_dir, "file1.txt") - with open(file_path, "r") as f: - content = f.read() - assert "Initial content" in content - assert "Modified content" not in content - - -def test_reset_step_files(): - repo_initializer = initialize_repo("tests/specs/reset_step/reset_step_files.yml") - with repo_initializer.initialize() as r: - staged_files = [d.a_path for d in r.index.diff("HEAD")] - assert "file1.txt" not in staged_files - assert "file2.txt" in staged_files diff --git a/tests/integration/steps/test_revert_step.py b/tests/integration/steps/test_revert_step.py deleted file mode 100644 index 31805a0..0000000 --- a/tests/integration/steps/test_revert_step.py +++ /dev/null @@ -1,102 +0,0 @@ -import os - -from shutil import copyfile -from git import Repo -from repo_smith.initialize_repo import initialize_repo -from tests.fixtures.git_fixtures import REMOTE_REPO_PATH, remote_repo - - -def test_revert_step_hash(remote_repo: Repo): - (REMOTE_REPO_PATH / "dummy1.txt").write_text("first") - remote_repo.index.add(["dummy1.txt"]) - remote_repo.index.commit("first commit") - - (REMOTE_REPO_PATH / "dummy2.txt").write_text("second") - remote_repo.index.add(["dummy2.txt"]) - remote_repo.index.commit("second commit") - - (REMOTE_REPO_PATH / "dummy3.txt").write_text("third") - remote_repo.index.add(["dummy3.txt"]) - remote_repo.index.commit("third commit") - - (REMOTE_REPO_PATH / "dummy4.txt").write_text("fourth") - remote_repo.index.add(["dummy4.txt"]) - remote_repo.index.commit("fourth commit") - - full_hash = remote_repo.commit("HEAD~1").hexsha - - copyfile( - "tests/specs/revert_step/revert_step_hash.yml", - "tests/specs/revert_step/temp-1.yml" - ) - - with open("tests/specs/revert_step/temp-1.yml", "a") as f: - f.write(full_hash) - - ir = initialize_repo("tests/specs/revert_step/temp-1.yml") - with ir.initialize() as r: - commits = list(r.iter_commits("main")) - commit = commits[0] - assert "Revert" in commit.message - - os.remove("tests/specs/revert_step/temp-1.yml") - - -def test_revert_step_short_hash(remote_repo: Repo): - (REMOTE_REPO_PATH / "dummy1.txt").write_text("first") - remote_repo.index.add(["dummy1.txt"]) - remote_repo.index.commit("first commit") - - (REMOTE_REPO_PATH / "dummy2.txt").write_text("second") - remote_repo.index.add(["dummy2.txt"]) - remote_repo.index.commit("second commit") - - (REMOTE_REPO_PATH / "dummy3.txt").write_text("third") - remote_repo.index.add(["dummy3.txt"]) - remote_repo.index.commit("third commit") - - (REMOTE_REPO_PATH / "dummy4.txt").write_text("fourth") - remote_repo.index.add(["dummy4.txt"]) - remote_repo.index.commit("fourth commit") - - short_hash = remote_repo.commit("HEAD~1").hexsha[:7] - - copyfile( - "tests/specs/revert_step/revert_step_short_hash.yml", - "tests/specs/revert_step/temp-2.yml" - ) - - with open("tests/specs/revert_step/temp-2.yml", "a") as f: - f.write(short_hash) - - ir = initialize_repo("tests/specs/revert_step/temp-2.yml") - with ir.initialize() as r: - commits = list(r.iter_commits("main")) - commit = commits[0] - assert "Revert" in commit.message - - os.remove("tests/specs/revert_step/temp-2.yml") - - -def test_revert_step_relative(remote_repo: Repo): - (REMOTE_REPO_PATH / "dummy1.txt").write_text("first") - remote_repo.index.add(["dummy1.txt"]) - remote_repo.index.commit("first commit") - - (REMOTE_REPO_PATH / "dummy2.txt").write_text("second") - remote_repo.index.add(["dummy2.txt"]) - remote_repo.index.commit("second commit") - - (REMOTE_REPO_PATH / "dummy3.txt").write_text("third") - remote_repo.index.add(["dummy3.txt"]) - remote_repo.index.commit("third commit") - - (REMOTE_REPO_PATH / "dummy4.txt").write_text("fourth") - remote_repo.index.add(["dummy4.txt"]) - remote_repo.index.commit("fourth commit") - - ir = initialize_repo("tests/specs/revert_step/revert_step_relative.yml") - with ir.initialize() as r: - commits = list(r.iter_commits("main")) - commit = commits[0] - assert "Revert" in commit.message diff --git a/tests/integration/steps/test_step.py b/tests/integration/steps/test_step.py deleted file mode 100644 index 59bc5d3..0000000 --- a/tests/integration/steps/test_step.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest - -from src.repo_smith.initialize_repo import initialize_repo - - -def test_step_missing_type(): - with pytest.raises(Exception): - initialize_repo("tests/specs/step_missing_type.yml") diff --git a/tests/integration/steps/test_tag_step.py b/tests/integration/steps/test_tag_step.py deleted file mode 100644 index b5bac26..0000000 --- a/tests/integration/steps/test_tag_step.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest -from src.repo_smith.initialize_repo import initialize_repo - - -def test_tag_step_missing_tag_name(): - with pytest.raises(Exception): - initialize_repo("tests/specs/tag_step/tag_step_missing_tag_name.yml") - - -def test_tag_step_empty_tag_name(): - with pytest.raises(Exception): - initialize_repo("tests/specs/tag_step/tag_step_empty_tag_name.yml") - - -def test_tag_step_invalid_tag_name(): - with pytest.raises(Exception): - initialize_repo("tests/specs/tag_step/tag_step_invalid_tag_name.yml") - - -def test_tag_step(): - repo_initializer = initialize_repo("tests/specs/tag_step/tag_step.yml") - with repo_initializer.initialize() as r: - assert len(r.tags) == 2 - assert r.tags[0].tag is not None - assert r.tags[0].tag.tag == "with-description" - assert r.tags[0].tag.message == "Hello world!" - assert r.tags[1].tag is None - assert str(r.tags[1]) == "without-description" diff --git a/tests/integration/test_clone_from.py b/tests/integration/test_clone_from.py deleted file mode 100644 index 32d356a..0000000 --- a/tests/integration/test_clone_from.py +++ /dev/null @@ -1,9 +0,0 @@ -from src.repo_smith.initialize_repo import initialize_repo - - -def test_clone_from(): - repo_initializer = initialize_repo("tests/specs/clone_from.yml") - with repo_initializer.initialize() as r: - commits = list(r.iter_commits("main")) - # Should be more than 1 (the empty commit we made) - assert len(commits) > 1 diff --git a/tests/integration/test_initialize_repo.py b/tests/integration/test_initialize_repo.py deleted file mode 100644 index 6bc4fa5..0000000 --- a/tests/integration/test_initialize_repo.py +++ /dev/null @@ -1,72 +0,0 @@ -import pytest -from git import Repo -from src.repo_smith.initialize_repo import initialize_repo - -# TODO: Test to make sure that the YAML parsing is accurate so we avoid individual -# integration test for every corner case covered in unit tests - - -def test_initialize_repo_missing_spec_path(): - with pytest.raises(ValueError, match="Invalid spec_path provided, not found."): - initialize_repo("tests/specs/invalid_spec_path_does_not_exist.yml") - - -def test_initialize_repo_incomplete_spec_file(): - with pytest.raises(ValueError, match="Incomplete spec file."): - initialize_repo("tests/specs/incomplete_spec_file.yml") - - -def test_initialize_repo_duplicate_ids(): - with pytest.raises( - ValueError, - match="ID commit is duplicated from a previous step. All IDs should be unique.", - ): - initialize_repo("tests/specs/duplicate_ids.yml") - - -def test_initialize_repo_duplicate_tags(): - with pytest.raises( - ValueError, - match="Tag tag is already in use by a previous step. All tag names should be unique.", - ): - initialize_repo("tests/specs/duplicate_tags.yml") - - -def test_initialize_repo_invalid_pre_hook(): - with pytest.raises(Exception): - repo_initializer = initialize_repo("tests/specs/basic_spec.yml") - repo_initializer.add_pre_hook("hello-world", lambda _: None) - - -def test_initialize_repo_invalid_post_hook(): - with pytest.raises(Exception): - repo_initializer = initialize_repo("tests/specs/basic_spec.yml") - repo_initializer.add_post_hook("hello-world", lambda _: None) - - -def test_initialize_repo_pre_hook(): - def initial_commit_pre_hook(_: Repo): - assert True - - repo_initializer = initialize_repo("tests/specs/basic_spec.yml") - repo_initializer.add_pre_hook("initial-commit", initial_commit_pre_hook) - with repo_initializer.initialize() as r: - assert r.commit("start-tag") is not None - - -def test_initialize_repo_post_hook(): - def initial_commit_post_hook(_: Repo): - assert True - - repo_initializer = initialize_repo("tests/specs/basic_spec.yml") - repo_initializer.add_post_hook("initial-commit", initial_commit_post_hook) - with repo_initializer.initialize(): - pass - - -def test_initialize_repo_basic_spec(): - initialize_repo("tests/specs/basic_spec.yml") - - -def test_initialize_repo_hooks(): - initialize_repo("tests/specs/hooks.yml") diff --git a/tests/unit/steps/test_add_step.py b/tests/unit/steps/test_add_step.py deleted file mode 100644 index 8f5ebff..0000000 --- a/tests/unit/steps/test_add_step.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from repo_smith.steps.add_step import AddStep - - -def test_add_step_parse_missing_files(): - with pytest.raises(ValueError, match='Missing "files" field in add step.'): - AddStep.parse("a", "d", "id", {}) - - -def test_add_step_parse_empty_files(): - with pytest.raises(ValueError, match='Empty "files" list in add step.'): - AddStep.parse("a", "d", "id", {"files": []}) - - -def test_add_step_parse(): - step = AddStep.parse("a", "d", "id", {"files": ["hello.txt"]}) - assert isinstance(step, AddStep) - assert step.name == "a" - assert step.description == "d" - assert step.id == "id" - assert step.files == ["hello.txt"] diff --git a/tests/unit/steps/test_bash_step.py b/tests/unit/steps/test_bash_step.py deleted file mode 100644 index d731376..0000000 --- a/tests/unit/steps/test_bash_step.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from repo_smith.steps.bash_step import BashStep - - -def test_bash_step_parse_missing_runs(): - with pytest.raises(ValueError, match='Missing "runs" field in bash step.'): - BashStep.parse("a", "d", "id", {}) - - -def test_bash_step_parse_empty_runs(): - with pytest.raises(ValueError, match='Empty "runs" field in bash step.'): - BashStep.parse("a", "d", "id", {"runs": ""}) - - -def test_bash_step_parse(): - step = BashStep.parse("a", "d", "id", {"runs": "ls"}) - assert isinstance(step, BashStep) - assert step.name == "a" - assert step.description == "d" - assert step.id == "id" - assert step.body == "ls" diff --git a/tests/unit/steps/test_branch_delete_step.py b/tests/unit/steps/test_branch_delete_step.py deleted file mode 100644 index 28823fc..0000000 --- a/tests/unit/steps/test_branch_delete_step.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest - -from repo_smith.steps.branch_delete_step import BranchDeleteStep - - -def test_branch_delete_step_parse_missing_branch_name(): - with pytest.raises( - ValueError, match='Missing "branch-name" field in branch-delete step.' - ): - BranchDeleteStep.parse("n", "d", "id", {}) - - -def test_branch_delete_step_parse_empty_branch_name(): - with pytest.raises( - ValueError, match='Empty "branch-name" field in branch-delete step.' - ): - BranchDeleteStep.parse("n", "d", "id", {"branch-name": ""}) - - -def test_branch_delete_step_parse(): - step = BranchDeleteStep.parse("n", "d", "id", {"branch-name": "test"}) - assert isinstance(step, BranchDeleteStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.branch_name == "test" - diff --git a/tests/unit/steps/test_branch_rename_step.py b/tests/unit/steps/test_branch_rename_step.py deleted file mode 100644 index 0ef245e..0000000 --- a/tests/unit/steps/test_branch_rename_step.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest - -from repo_smith.steps.branch_delete_step import BranchDeleteStep -from repo_smith.steps.branch_rename_step import BranchRenameStep - - -def test_branch_rename_step_parse_missing_branch_name(): - with pytest.raises( - ValueError, match='Missing "branch-name" field in branch-rename step.' - ): - BranchRenameStep.parse("n", "d", "id", {}) - - -def test_branch_rename_step_parse_empty_branch_name(): - with pytest.raises( - ValueError, match='Empty "branch-name" field in branch-rename step.' - ): - BranchRenameStep.parse("n", "d", "id", {"branch-name": ""}) - - -def test_branch_rename_step_parse_missing_new_name(): - with pytest.raises( - ValueError, match='Missing "new-name" field in branch-rename step.' - ): - BranchRenameStep.parse("n", "d", "id", {"branch-name": "test"}) - - -def test_branch_rename_step_parse_empty_new_name(): - with pytest.raises( - ValueError, match='Empty "new-name" field in branch-rename step.' - ): - BranchRenameStep.parse("n", "d", "id", {"branch-name": "test", "new-name": ""}) - - -def test_branch_rename_step_parse(): - step = BranchRenameStep.parse( - "n", "d", "id", {"branch-name": "test", "new-name": "other"} - ) - assert isinstance(step, BranchRenameStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.original_branch_name == "test" - assert step.target_branch_name == "other" diff --git a/tests/unit/steps/test_branch_step.py b/tests/unit/steps/test_branch_step.py deleted file mode 100644 index 3c98f5c..0000000 --- a/tests/unit/steps/test_branch_step.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from repo_smith.steps.branch_step import BranchStep - - -def test_branch_step_parse_missing_branch_name(): - with pytest.raises(ValueError, match='Missing "branch-name" field in branch step.'): - BranchStep.parse("n", "d", "id", {}) - - -def test_branch_step_parse_empty_branch_name(): - with pytest.raises(ValueError, match='Empty "branch-name" field in branch step.'): - BranchStep.parse("n", "d", "id", {"branch-name": ""}) - - -def test_branch_step_parse(): - step = BranchStep.parse("n", "d", "id", {"branch-name": "test"}) - assert isinstance(step, BranchStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.branch_name == "test" diff --git a/tests/unit/steps/test_checkout_step.py b/tests/unit/steps/test_checkout_step.py deleted file mode 100644 index 9d77954..0000000 --- a/tests/unit/steps/test_checkout_step.py +++ /dev/null @@ -1,81 +0,0 @@ -import pytest - -from repo_smith.steps.checkout_step import CheckoutStep - - -def test_checkout_step_parse_missing_branch_name_and_commit_hash(): - with pytest.raises( - ValueError, - match='Provide either "branch-name" or "commit-hash" in checkout step.', - ): - CheckoutStep.parse("n", "d", "id", {}) - - -def test_checkout_step_parse_both_branch_name_and_commit_hash(): - with pytest.raises( - ValueError, - match='Provide either "branch-name" or "commit-hash", not both, in checkout step.', - ): - CheckoutStep.parse( - "n", "d", "id", {"branch-name": "test", "commit-hash": "abc123"} - ) - - -def test_checkout_step_parse_start_point_with_commit_hash(): - with pytest.raises( - ValueError, - match='"start-point" field requires "branch-name" field to be provided in checkout step.', - ): - CheckoutStep.parse( - "n", "d", "id", {"start-point": "HEAD~1", "commit-hash": "abc123"} - ) - - -def test_checkout_step_parse_empty_branch_name(): - with pytest.raises(ValueError, match='Empty "branch-name" field in checkout step.'): - CheckoutStep.parse("n", "d", "id", {"branch-name": ""}) - - -def test_checkout_step_parse_empty_commit_hash(): - with pytest.raises(ValueError, match='Empty "commit-hash" field in checkout step.'): - CheckoutStep.parse("n", "d", "id", {"commit-hash": ""}) - - -def test_checkout_step_parse_empty_start_point(): - with pytest.raises(ValueError, match='Empty "start-point" field in checkout step.'): - CheckoutStep.parse("n", "d", "id", {"start-point": "", "branch-name": "test"}) - - -def test_checkout_step_parse_with_branch_name(): - step = CheckoutStep.parse("n", "d", "id", {"branch-name": "test"}) - assert isinstance(step, CheckoutStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.branch_name == "test" - assert step.commit_hash is None - assert step.start_point is None - - -def test_checkout_step_parse_with_commit_hash(): - step = CheckoutStep.parse("n", "d", "id", {"commit-hash": "abc123"}) - assert isinstance(step, CheckoutStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.branch_name is None - assert step.commit_hash == "abc123" - assert step.start_point is None - - -def test_checkout_step_parse_with_start_point(): - step = CheckoutStep.parse( - "n", "d", "id", {"branch-name": "test", "start-point": "HEAD~1"} - ) - assert isinstance(step, CheckoutStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.branch_name == "test" - assert step.commit_hash is None - assert step.start_point == "HEAD~1" diff --git a/tests/unit/steps/test_commit_step.py b/tests/unit/steps/test_commit_step.py deleted file mode 100644 index cc2b3f4..0000000 --- a/tests/unit/steps/test_commit_step.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest - -from repo_smith.steps.commit_step import CommitStep - - -def test_commit_step_parse_missing_message(): - with pytest.raises(ValueError, match='Missing "message" field in commit step.'): - CommitStep.parse("n", "d", "id", {}) - - -def test_commit_step_parse_empty_message(): - with pytest.raises(ValueError, match='Empty "message" field in commit step.'): - CommitStep.parse("n", "d", "id", {"message": ""}) - - -def test_commit_step_parse_missing_empty(): - step = CommitStep.parse("n", "d", "id", {"message": "Test"}) - assert isinstance(step, CommitStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.message == "Test" - assert not step.empty - - -def test_commit_step_parse_with_empty(): - step = CommitStep.parse("n", "d", "id", {"message": "Test", "empty": True}) - assert isinstance(step, CommitStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.message == "Test" - assert step.empty diff --git a/tests/unit/steps/test_dispatcher.py b/tests/unit/steps/test_dispatcher.py deleted file mode 100644 index c3000b7..0000000 --- a/tests/unit/steps/test_dispatcher.py +++ /dev/null @@ -1,74 +0,0 @@ -from unittest.mock import patch - -import pytest -from repo_smith.steps.add_step import AddStep -from repo_smith.steps.bash_step import BashStep -from repo_smith.steps.branch_delete_step import BranchDeleteStep -from repo_smith.steps.branch_rename_step import BranchRenameStep -from repo_smith.steps.branch_step import BranchStep -from repo_smith.steps.checkout_step import CheckoutStep -from repo_smith.steps.commit_step import CommitStep -from repo_smith.steps.dispatcher import Dispatcher -from repo_smith.steps.fetch_step import FetchStep -from repo_smith.steps.file_step import ( - AppendFileStep, - DeleteFileStep, - EditFileStep, - NewFileStep, -) -from repo_smith.steps.merge_step import MergeStep -from repo_smith.steps.remote_step import RemoteStep -from repo_smith.steps.step_type import StepType -from repo_smith.steps.tag_step import TagStep - - -def test_dispatch_calls_correct_step_parse(): - step_dict = { - "type": "commit", - "name": "my commit", - "description": "desc", - "id": "123", - } - - # Patch CommitStep.parse so we don't run real logic - with patch( - "repo_smith.steps.commit_step.CommitStep.parse", return_value="parsed" - ) as mock_parse: - result = Dispatcher.dispatch(step_dict) - - # parse should have been called once with correct arguments - mock_parse.assert_called_once_with("my commit", "desc", "123", step_dict) - - # The dispatcher should return whatever parse returned - assert result == "parsed" - - -def test_dispatch_missing_type_raises(): - step_dict = {"name": "no type"} - with pytest.raises(ValueError, match='Missing "type" field in step.'): - Dispatcher.dispatch(step_dict) - - -STEP_TYPES_TO_CLASSES = { - StepType.COMMIT: CommitStep, - StepType.ADD: AddStep, - StepType.TAG: TagStep, - StepType.NEW_FILE: NewFileStep, - StepType.EDIT_FILE: EditFileStep, - StepType.DELETE_FILE: DeleteFileStep, - StepType.APPEND_FILE: AppendFileStep, - StepType.BASH: BashStep, - StepType.BRANCH: BranchStep, - StepType.BRANCH_RENAME: BranchRenameStep, - StepType.BRANCH_DELETE: BranchDeleteStep, - StepType.CHECKOUT: CheckoutStep, - StepType.REMOTE: RemoteStep, - StepType.MERGE: MergeStep, - StepType.FETCH: FetchStep, -} - - -@pytest.mark.parametrize("step_type, step_path", STEP_TYPES_TO_CLASSES.items()) -def test_get_type_returns_correct_class(step_type, step_path): - # Uses mangled name: https://stackoverflow.com/questions/2064202/private-members-in-python - assert Dispatcher._Dispatcher__get_type(step_type) is step_path # type: ignore diff --git a/tests/unit/steps/test_fetch_step.py b/tests/unit/steps/test_fetch_step.py deleted file mode 100644 index fc5510d..0000000 --- a/tests/unit/steps/test_fetch_step.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from repo_smith.steps.fetch_step import FetchStep - - -def test_fetch_step_parse_missing_remote_name(): - with pytest.raises(ValueError, match='Missing "remote-name" field in fetch step.'): - FetchStep.parse("n", "d", "id", {}) - - -def test_fetch_step_parse_empty_remote_name(): - with pytest.raises(ValueError, match='Empty "remote-name" field in fetch step.'): - FetchStep.parse("n", "d", "id", {"remote-name": ""}) - - -def test_commit_step_parse_with_empty(): - step = FetchStep.parse("n", "d", "id", {"remote-name": "test"}) - assert isinstance(step, FetchStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.remote_name == "test" diff --git a/tests/unit/steps/test_file_step.py b/tests/unit/steps/test_file_step.py deleted file mode 100644 index 50b1304..0000000 --- a/tests/unit/steps/test_file_step.py +++ /dev/null @@ -1,48 +0,0 @@ -import pytest -from repo_smith.steps.file_step import ( - AppendFileStep, - DeleteFileStep, - EditFileStep, - FileStep, - NewFileStep, -) - - -def test_file_step_get_details_missing_filename(): - with pytest.raises(ValueError, match='Missing "filename" field in file step.'): - FileStep.get_details({}) - - -def test_file_step_get_details_empty_filename(): - with pytest.raises(ValueError, match='Empty "filename" field in file step.'): - FileStep.get_details({"filename": ""}) - - -def test_file_step_get_details_missing_contents(): - filename, contents = FileStep.get_details({"filename": "hello.txt"}) - assert filename == "hello.txt" - assert contents == "" - - -def test_file_step_get_details(): - filename, contents = FileStep.get_details( - {"filename": "hello.txt", "contents": "Hello world"} - ) - assert filename == "hello.txt" - assert contents == "Hello world" - - -FILE_STEP_CLASSES = [NewFileStep, EditFileStep, AppendFileStep, DeleteFileStep] - - -@pytest.mark.parametrize("step_class", FILE_STEP_CLASSES) -def test_new_file_step_parse(step_class): - step = step_class.parse( - "n", "d", "id", {"filename": "hello.txt", "contents": "Hello world"} - ) - assert isinstance(step, step_class) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.filename == "hello.txt" - assert step.contents == "Hello world" diff --git a/tests/unit/steps/test_merge_step.py b/tests/unit/steps/test_merge_step.py deleted file mode 100644 index 05816f1..0000000 --- a/tests/unit/steps/test_merge_step.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest - -from repo_smith.steps.merge_step import MergeStep - - -def test_merge_step_parse_missing_branch_name(): - with pytest.raises(ValueError, match='Missing "branch-name" field in merge step.'): - MergeStep.parse("n", "d", "id", {}) - - -def test_merge_step_parse_empty_branch_name(): - with pytest.raises(ValueError, match='Empty "branch-name" field in merge step.'): - MergeStep.parse("n", "d", "id", {"branch-name": ""}) - - -MERGE_STEP_CONFIGURATION = { - "no-ff not set, squash not set": {"branch-name": "test"}, - "no-ff not set, squash set": {"branch-name": "test", "squash": True}, - "no-ff set, squash not set": {"branch-name": "test", "no-ff": True}, - "no-ff set, squash set": {"branch-name": "test", "squash": True, "no-ff": True}, -} - - -@pytest.mark.parametrize("config_name, config", MERGE_STEP_CONFIGURATION.items()) -def test_merge_step_parse(config_name, config): - step = MergeStep.parse("n", "d", "id", config) - assert isinstance(step, MergeStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.branch_name == "test" - assert step.no_fast_forward == config.get("no-ff", False) - assert step.squash == config.get("squash", False) diff --git a/tests/unit/steps/test_remote_step.py b/tests/unit/steps/test_remote_step.py deleted file mode 100644 index b3eb94f..0000000 --- a/tests/unit/steps/test_remote_step.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest - -from repo_smith.steps.remote_step import RemoteStep - - -def test_remote_step_parse_missing_remote_url(): - with pytest.raises(ValueError, match='Missing "remote-url" field in remote step.'): - RemoteStep.parse("n", "d", "id", {}) - - -def test_remote_step_parse_empty_remote_url(): - with pytest.raises(ValueError, match='Empty "remote-url" field in remote step.'): - RemoteStep.parse("n", "d", "id", {"remote-url": ""}) - - -def test_remote_step_parse_missing_remote_name(): - with pytest.raises(ValueError, match='Missing "remote-name" field in remote step.'): - RemoteStep.parse("n", "d", "id", {"remote-url": "https://test.com"}) - - -def test_remote_step_parse_empty_remote_name(): - with pytest.raises(ValueError, match='Empty "remote-name" field in remote step.'): - RemoteStep.parse( - "n", "d", "id", {"remote-url": "https://test.com", "remote-name": ""} - ) - - -def test_remote_step_parse(): - step = RemoteStep.parse( - "n", "d", "id", {"remote-url": "https://test.com", "remote-name": "upstream"} - ) - assert isinstance(step, RemoteStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.remote_url == "https://test.com" - assert step.remote_name == "upstream" diff --git a/tests/unit/steps/test_reset_step.py b/tests/unit/steps/test_reset_step.py deleted file mode 100644 index 653252d..0000000 --- a/tests/unit/steps/test_reset_step.py +++ /dev/null @@ -1,76 +0,0 @@ -import pytest - -from repo_smith.steps.reset_step import ResetStep - - -def test_reset_step_parse_with_revision_and_mode(): - step = ResetStep.parse("n", "d", "id", {"revision": "HEAD~1", "mode": "mixed"}) - assert isinstance(step, ResetStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.revision == "HEAD~1" - assert step.mode == "mixed" - assert step.files is None - - -def test_reset_step_parse_with_files(): - step = ResetStep.parse( - "n", - "d", - "id", - {"revision": "HEAD", "mode": "mixed", "files": ["file1.txt", "file2.txt"]}, - ) - assert step.revision == "HEAD" - assert step.mode == "mixed" - assert step.files == ["file1.txt", "file2.txt"] - - -def test_reset_step_parse_missing_mode(): - with pytest.raises(ValueError, match='Missing "mode" field in reset step.'): - ResetStep.parse("n", "d", "id", {"revision": "HEAD~1"}) - - -def test_reset_step_parse_missing_revision(): - with pytest.raises(ValueError, match='Missing "revision" field in reset step.'): - ResetStep.parse("n", "d", "id", {"mode": "hard"}) - - -def test_reset_step_parse_empty_revision(): - with pytest.raises(ValueError, match='Empty "revision" field in reset step.'): - ResetStep.parse("n", "d", "id", {"revision": "", "mode": "hard"}) - - -def test_reset_step_parse_invalid_mode(): - with pytest.raises( - ValueError, - match="Invalid \"mode\" value. Must be one of: \\('soft', 'mixed', 'hard'\\).", - ): - ResetStep.parse("n", "d", "id", {"revision": "HEAD~1", "mode": "invalid"}) - - -def test_reset_step_parse_empty_files_list(): - with pytest.raises(ValueError, match='Empty "files" list in reset step.'): - ResetStep.parse( - "n", "d", "id", {"revision": "HEAD", "mode": "mixed", "files": []} - ) - - -def test_reset_step_parse_files_with_soft_mode(): - with pytest.raises( - ValueError, - match='Cannot use "files" field with "soft" mode in reset step. Only "mixed" mode is allowed with files.', - ): - ResetStep.parse( - "n", "d", "id", {"revision": "HEAD", "mode": "soft", "files": ["file.txt"]} - ) - - -def test_reset_step_parse_files_with_hard_mode(): - with pytest.raises( - ValueError, - match='Cannot use "files" field with "hard" mode in reset step. Only "mixed" mode is allowed with files.', - ): - ResetStep.parse( - "n", "d", "id", {"revision": "HEAD", "mode": "hard", "files": ["file.txt"]} - ) diff --git a/tests/unit/steps/test_revert_step.py b/tests/unit/steps/test_revert_step.py deleted file mode 100644 index d730556..0000000 --- a/tests/unit/steps/test_revert_step.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from repo_smith.steps.revert_step import RevertStep - - -def test_add_step_parse_missing_files(): - with pytest.raises(ValueError, match='Missing "revision" field in revert step.'): - RevertStep.parse("n", "d", "id", {}) - - -def test_add_step_parse_empty_files(): - with pytest.raises(ValueError, match='Empty "revision" field in revert step.'): - RevertStep.parse("n", "d", "id", {"revision": ""}) - - -def test_revert_step_parse(): - step = RevertStep.parse("n", "d", "id", {"revision": "HEAD~4"}) - assert isinstance(step, RevertStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.revision == "HEAD~4" diff --git a/tests/unit/steps/test_step_type.py b/tests/unit/steps/test_step_type.py deleted file mode 100644 index e9a8ab0..0000000 --- a/tests/unit/steps/test_step_type.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest - -from repo_smith.steps.step_type import StepType - - -def test_step_type_uncovered_type(): - with pytest.raises(ValueError): - StepType.from_value("this should not be implemented") diff --git a/tests/unit/steps/test_tag_step.py b/tests/unit/steps/test_tag_step.py deleted file mode 100644 index 71d98e8..0000000 --- a/tests/unit/steps/test_tag_step.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -from repo_smith.steps.tag_step import TagStep - - -def test_tag_step_parse_missing_tag_name(): - with pytest.raises(ValueError, match='Missing "tag-name" field in tag step.'): - TagStep.parse("n", "d", "id", {}) - - -def test_tag_step_parse_empty_tag_name(): - with pytest.raises(ValueError, match='Empty "tag-name" field in tag step.'): - TagStep.parse("n", "d", "id", {"tag-name": ""}) - - -def test_tag_step_parse_invalid_tag_name(): - with pytest.raises( - ValueError, - match='Field "tag-name" can only contain alphanumeric characters, _, -, .', - ): - TagStep.parse("n", "d", "id", {"tag-name": "(open)"}) - - -def test_tag_step_parse_missing_tag_message(): - step = TagStep.parse("n", "d", "id", {"tag-name": "start"}) - assert isinstance(step, TagStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.tag_name == "start" - assert step.tag_message is None - - -def test_tag_step_parse_with_tag_message(): - step = TagStep.parse( - "n", "d", "id", {"tag-name": "start", "tag-message": "this is a message"} - ) - assert isinstance(step, TagStep) - assert step.name == "n" - assert step.description == "d" - assert step.id == "id" - assert step.tag_name == "start" - assert step.tag_message == "this is a message"