diff --git a/side_track/.gitmastery-exercise.json b/side_track/.gitmastery-exercise.json new file mode 100644 index 0000000..c93a1f9 --- /dev/null +++ b/side_track/.gitmastery-exercise.json @@ -0,0 +1,17 @@ +{ + "exercise_name": "side-track", + "tags": [ + "git-branch", + "git-checkout" + ], + "requires_git": true, + "requires_github": false, + "base_files": {}, + "exercise_repo": { + "repo_type": "local", + "repo_name": "branch-me", + "repo_title": null, + "create_fork": null, + "init": true + } +} \ No newline at end of file diff --git a/side_track/README.md b/side_track/README.md new file mode 100644 index 0000000..5fb1935 --- /dev/null +++ b/side_track/README.md @@ -0,0 +1,27 @@ +# side-track + +You realized that something is broken with your project and want to fix it. But you are also currently working on a major feature on `main` right now +and you don't want to push that major feature yet. + +## Task + +### Task 1 + +Create a bug fix branch named `bug-fix` and go to the branch. + +### Task 2 + +On `bug-fix`, open the `greet.py` file and ensure that the `greet` function prints the `name` variable, not just `Alice`. + +Commit and save the fix. + +### Task 3 + +On `bug-fix`, open the `calculator.py` file and ensure that the `add` function returns the sum of two numbers. + +Commit and save the fix. + +### Task 4 + +Return to the `main` branch where you're working on your major feature. + diff --git a/side_track/__init__.py b/side_track/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/side_track/download.py b/side_track/download.py new file mode 100644 index 0000000..bda4fd8 --- /dev/null +++ b/side_track/download.py @@ -0,0 +1,32 @@ +import subprocess +from sys import exit +from typing import List, Optional + +__resources__ = {"calculator.py": "calculator.py", "greet.py": "greet.py"} + + +def run_command(command: List[str], verbose: bool) -> Optional[str]: + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + check=True, + ) + if verbose: + print(result.stdout) + return result.stdout + except subprocess.CalledProcessError as e: + if verbose: + print(e.stderr) + exit(1) + + +def setup(verbose: bool = False): + commits_str = run_command( + ["git", "log", "--reverse", "--pretty=format:%h"], verbose + ) + assert commits_str is not None + first_commit = commits_str.split("\n")[0] + tag_name = f"git-mastery-start-{first_commit}" + run_command(["git", "tag", tag_name], verbose) diff --git a/side_track/res/calculator.py b/side_track/res/calculator.py new file mode 100644 index 0000000..52c68ff --- /dev/null +++ b/side_track/res/calculator.py @@ -0,0 +1,14 @@ +def add(a, b): + return a - b + + +def subtract(a, b): + return a - b + + +def divide(a, b): + return a / b + + +def multiply(a, b): + return a * b diff --git a/side_track/res/greet.py b/side_track/res/greet.py new file mode 100644 index 0000000..f6424c7 --- /dev/null +++ b/side_track/res/greet.py @@ -0,0 +1,8 @@ +def greet(name): + # TODO: Fix this to print name, not just Alice all the time + print("Hi Alice") + + +greet("John") +greet("Joe") +greet("Alice") diff --git a/side_track/tests/__init__.py b/side_track/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/side_track/tests/specs/add_not_fixed.yml b/side_track/tests/specs/add_not_fixed.yml new file mode 100644 index 0000000..c3ea7fc --- /dev/null +++ b/side_track/tests/specs/add_not_fixed.yml @@ -0,0 +1,29 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty + id: start + - type: branch + branch-name: bug-fix + - type: new-file + filename: greet.py + contents: | + def greet(name): + print("Hi " + name) + - type: new-file + filename: calculator.py + contents: | + def add(a, b): + return a - b + - type: commit + empty: true + message: Empty + - type: add + files: + - greet.py + - calculator.py + - type: commit + message: Add + - type: checkout + branch-name: main diff --git a/side_track/tests/specs/base.yml b/side_track/tests/specs/base.yml new file mode 100644 index 0000000..00c3a53 --- /dev/null +++ b/side_track/tests/specs/base.yml @@ -0,0 +1,6 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start diff --git a/side_track/tests/specs/greet_not_fixed.yml b/side_track/tests/specs/greet_not_fixed.yml new file mode 100644 index 0000000..3289528 --- /dev/null +++ b/side_track/tests/specs/greet_not_fixed.yml @@ -0,0 +1,29 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty + id: start + - type: branch + branch-name: bug-fix + - type: new-file + filename: greet.py + contents: | + def greet(name): + print("Hi Alice") + - type: new-file + filename: calculator.py + contents: | + def add(a, b): + return a + b + - type: commit + empty: true + message: Empty + - type: add + files: + - greet.py + - calculator.py + - type: commit + message: Add + - type: checkout + branch-name: main diff --git a/side_track/tests/specs/missing_commits.yml b/side_track/tests/specs/missing_commits.yml new file mode 100644 index 0000000..797f913 --- /dev/null +++ b/side_track/tests/specs/missing_commits.yml @@ -0,0 +1,13 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty + id: start + - type: branch + branch-name: bug-fix + - type: commit + empty: true + message: Empty + - type: checkout + branch-name: main diff --git a/side_track/tests/specs/no_bug_fix.yml b/side_track/tests/specs/no_bug_fix.yml new file mode 100644 index 0000000..475577f --- /dev/null +++ b/side_track/tests/specs/no_bug_fix.yml @@ -0,0 +1,6 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty + id: start diff --git a/side_track/tests/specs/not_main.yml b/side_track/tests/specs/not_main.yml new file mode 100644 index 0000000..67e2f4b --- /dev/null +++ b/side_track/tests/specs/not_main.yml @@ -0,0 +1,8 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty + id: start + - type: branch + branch-name: bug-fix diff --git a/side_track/tests/specs/uncommitted.yml b/side_track/tests/specs/uncommitted.yml new file mode 100644 index 0000000..81a1de1 --- /dev/null +++ b/side_track/tests/specs/uncommitted.yml @@ -0,0 +1,16 @@ +initialization: + steps: + - type: new-file + filename: test.txt + contents: | + hi + - type: add + files: + - test.txt + - type: commit + message: Start + id: start + - type: edit-file + filename: test.txt + contents: | + changed diff --git a/side_track/tests/test_verify.py b/side_track/tests/test_verify.py new file mode 100644 index 0000000..ba59d3c --- /dev/null +++ b/side_track/tests/test_verify.py @@ -0,0 +1,48 @@ +from git_autograder import GitAutograderStatus, GitAutograderTestLoader +from git_autograder.test_utils import assert_output + +from ..verify import ( + CALCULATOR_NOT_FIXED, + GREET_NOT_FIXED, + MISSING_BUG_FIX_BRANCH, + MISSING_COMMITS, + NOT_ON_MAIN, + UNCOMMITTED_CHANGES, + verify, +) + +REPOSITORY_NAME = "side-track" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_uncommitted(): + with loader.load("specs/uncommitted.yml") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [UNCOMMITTED_CHANGES]) + + +def test_not_main(): + with loader.load("specs/not_main.yml") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [NOT_ON_MAIN]) + + +def test_no_bug_fix(): + with loader.load("specs/no_bug_fix.yml") as output: + assert_output( + output, GitAutograderStatus.UNSUCCESSFUL, [MISSING_BUG_FIX_BRANCH] + ) + + +def test_missing_commits(): + with loader.load("specs/missing_commits.yml") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [MISSING_COMMITS]) + + +def test_greet_not_fixed(): + with loader.load("specs/greet_not_fixed.yml") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [GREET_NOT_FIXED]) + + +def test_add_not_fixed(): + with loader.load("specs/add_not_fixed.yml") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [CALCULATOR_NOT_FIXED]) diff --git a/side_track/verify.py b/side_track/verify.py new file mode 100644 index 0000000..c28bbdd --- /dev/null +++ b/side_track/verify.py @@ -0,0 +1,102 @@ +import io +import sys +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path +from typing import Any, Dict, Optional + +from git_autograder import ( + GitAutograderExercise, + GitAutograderInvalidStateException, + GitAutograderOutput, + GitAutograderStatus, + GitAutograderWrongAnswerException, +) + +MISSING_BUG_FIX_BRANCH = "You are missing the bug-fix branch" +MISSING_COMMITS = "You do not have 2 commits on the bug-fix branch" +UNCOMMITTED_CHANGES = "You still have uncommitted changes. Commit them first on the appropriate branch first!" +NOT_ON_MAIN = ( + "You aren't currently on the main branch. Checkout to that branch and try again!" +) +DETACHED_HEAD = "You should not be in a detached HEAD state! Run git checkout main to get back to main" +GREET_NOT_FIXED = "You have not fixed the greet function in greet.py" +CALCULATOR_NOT_FIXED = "You have not fixed the add function in calculator.py" + + +def execute_function( + filepath: str | Path, func_name: str, args: Dict[str, Any] +) -> Optional[Any]: + with open(filepath, "r") as f: + sys.dont_write_bytecode = True + code = f.read() + namespace: Dict = {} + exec(code, namespace) + result = namespace[func_name](**args) + sys.dont_write_bytecode = False + return result + + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + main_branch = exercise.repo.branches.branch("main") + try: + if exercise.repo.repo.is_dirty(): + raise exercise.wrong_answer([UNCOMMITTED_CHANGES]) + + try: + if exercise.repo.repo.active_branch.name != "main": + raise exercise.wrong_answer([NOT_ON_MAIN]) + except TypeError: + raise exercise.wrong_answer([DETACHED_HEAD]) + + if not exercise.repo.branches.has_branch("bug-fix"): + raise exercise.wrong_answer([MISSING_BUG_FIX_BRANCH]) + + bug_fix_branch = exercise.repo.branches.branch("bug-fix") + bug_fix_branch.checkout() + if len(bug_fix_branch.user_commits) < 2: + raise exercise.wrong_answer([MISSING_COMMITS]) + + # Ensure that they applied the right fix by testing the greet function + fixed_greet = True + for name in ["James", "Hi", "Alice", "Bob"]: + buf = io.StringIO() + with redirect_stdout(buf): + execute_function( + Path(exercise.repo.repo_path) / "greet.py", "greet", {"name": name} + ) + if buf.getvalue().strip() != f"Hi {name}": + fixed_greet = False + break + + fixed_calculator = True + for a, b in zip([1, 2, 3, 4, 5], [11, 123, 9, 10, 1]): + result = execute_function( + Path(exercise.repo.repo_path) / "calculator.py", "add", {"a": a, "b": b} + ) + if result is None: + fixed_calculator = False + break + if result != a + b: + fixed_calculator = False + break + + comments = [] + if not fixed_greet: + comments.append(GREET_NOT_FIXED) + if not fixed_calculator: + comments.append(CALCULATOR_NOT_FIXED) + + if comments: + raise exercise.wrong_answer(comments) + + return exercise.to_output( + ["Great work with using git branch and git checkout to fix the bugs!"], + GitAutograderStatus.SUCCESSFUL, + ) + except (GitAutograderWrongAnswerException, GitAutograderInvalidStateException): + raise + except Exception as e: + print(e) + raise exercise.wrong_answer(["Something bad happened"]) + finally: + main_branch.checkout()