diff --git a/branch_compare/.gitmastery-exercise.json b/branch_compare/.gitmastery-exercise.json new file mode 100644 index 00000000..bdd599dc --- /dev/null +++ b/branch_compare/.gitmastery-exercise.json @@ -0,0 +1,16 @@ +{ + "exercise_name": "branch-compare", + "tags": ["git-branch", "git-diff"], + "requires_git": true, + "requires_github": false, + "base_files": { + "answers.txt": "answers.txt" + }, + "exercise_repo": { + "repo_type": "local", + "repo_name": "data-streams", + "repo_title": null, + "create_fork": null, + "init": true + } +} diff --git a/branch_compare/README.md b/branch_compare/README.md new file mode 100644 index 00000000..37b6f0a5 --- /dev/null +++ b/branch_compare/README.md @@ -0,0 +1 @@ +See https://git-mastery.github.io/lessons/branch/exercise-branch-compare.html diff --git a/branch_compare/__init__.py b/branch_compare/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/branch_compare/download.py b/branch_compare/download.py new file mode 100644 index 00000000..fe88ddec --- /dev/null +++ b/branch_compare/download.py @@ -0,0 +1,55 @@ +from exercise_utils.git import add, commit, checkout +from exercise_utils.file import append_to_file, create_or_update_file + +import random + + +def get_sequence(n=1000, digits=8, seed=None): + rng = random.Random(seed) + lo, hi = 10 ** (digits - 1), 10**digits - 1 + return rng.sample(range(lo, hi + 1), k=n) + + +def get_modified_sequence(seq, digits=8, idx=None, seed=None): + rng = random.Random(seed) + n = len(seq) + if idx is None: + idx = rng.randrange(n) + + modified = seq.copy() + seen = set(seq) + lo, hi = 10 ** (digits - 1), 10**digits - 1 + + old = modified[idx] + new = old + while new in seen: + new = rng.randint(lo, hi) + modified[idx] = new + return modified + + +def setup(verbose: bool = False): + orig_data = get_sequence() + modified_data = get_modified_sequence(orig_data) + + create_or_update_file("data.txt", "") + add(["data.txt"], verbose) + commit("Add empty data.txt", verbose) + checkout("stream-1", True, verbose) + + join_orig_data = "\n".join(map(str, orig_data)) + append_to_file("data.txt", join_orig_data) + + add(["data.txt"], verbose) + commit("Add data to data.txt", verbose) + + checkout("main", False, verbose) + checkout("stream-2", True, verbose) + + join_modified_data = "\n".join(map(str, modified_data)) + append_to_file("data.txt", join_modified_data) + + add(["data.txt"], verbose) + commit("Add data to data.txt", verbose) + + checkout("main", False, verbose) diff --git a/branch_compare/res/answers.txt b/branch_compare/res/answers.txt new file mode 100644 index 00000000..37cc88d3 --- /dev/null +++ b/branch_compare/res/answers.txt @@ -0,0 +1,5 @@ +Q: Which numbers are present in stream-1 but not in stream-2? +A: + +Q: Which numbers are present in stream-2 but not in stream-1? +A: diff --git a/branch_compare/tests/__init__.py b/branch_compare/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/branch_compare/tests/specs/base.yml b/branch_compare/tests/specs/base.yml new file mode 100644 index 00000000..57146800 --- /dev/null +++ b/branch_compare/tests/specs/base.yml @@ -0,0 +1,43 @@ +initialization: + steps: + - type: commit + empty: true + message: Set initial state + id: start + - type: commit + empty: true + message: Add empty data.txt + + - type: branch + branch-name: stream-1 + - type: new-file + filename: data.txt + contents: | + 11111 + 22222 + 12345 + - type: add + files: + - data.txt + - type: commit + message: Add data to data.txt + + - type: checkout + branch-name: main + + - type: branch + branch-name: stream-2 + - type: new-file + filename: data.txt + contents: | + 11111 + 22222 + 98765 + - type: add + files: + - data.txt + - type: commit + message: Add data to data.txt + + - type: checkout + branch-name: main diff --git a/branch_compare/tests/specs/extra_commit_on_stream1.yml b/branch_compare/tests/specs/extra_commit_on_stream1.yml new file mode 100644 index 00000000..6e01495b --- /dev/null +++ b/branch_compare/tests/specs/extra_commit_on_stream1.yml @@ -0,0 +1,55 @@ +initialization: + steps: + - type: commit + empty: true + message: Set initial state + id: start + - type: commit + empty: true + message: Add empty data.txt + + - type: branch + branch-name: stream-1 + - type: new-file + filename: data.txt + contents: | + 11111 + 22222 + 12345 + - type: add + files: + - data.txt + - type: commit + message: Add data to data.txt + + - type: checkout + branch-name: main + + - type: branch + branch-name: stream-2 + - type: new-file + filename: data.txt + contents: | + 11111 + 22222 + 98765 + - type: add + files: + - data.txt + - type: commit + message: Add data to data.txt + + - type: checkout + branch-name: stream-1 + - type: new-file + filename: extra.txt + contents: | + extra content + - type: add + files: + - extra.txt + - type: commit + message: Extra change on stream-1 + + - type: checkout + branch-name: main diff --git a/branch_compare/tests/test_verify.py b/branch_compare/tests/test_verify.py new file mode 100644 index 00000000..90dbbcd0 --- /dev/null +++ b/branch_compare/tests/test_verify.py @@ -0,0 +1,57 @@ +from git_autograder import GitAutograderStatus, GitAutograderTestLoader, assert_output +from git_autograder.answers.rules.has_exact_value_rule import HasExactValueRule + +from ..verify import verify, QUESTION_ONE, QUESTION_TWO, NO_CHANGES_ERROR + +REPOSITORY_NAME = "branch-compare" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_base(): + with loader.load( + "specs/base.yml", + "start", + mock_answers={ + QUESTION_ONE: "12345", + QUESTION_TWO: "98765", + }, + ) as output: + assert_output(output, GitAutograderStatus.SUCCESSFUL) + + +def test_wrong_stream1_diff(): + with loader.load( + "specs/base.yml", + "start", + mock_answers={ + QUESTION_ONE: "99999", + QUESTION_TWO: "98765", + }, + ) as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [HasExactValueRule.NOT_EXACT.format(question=QUESTION_ONE)], + ) + + +def test_wrong_stream2_diff(): + with loader.load( + "specs/base.yml", + "start", + mock_answers={ + QUESTION_ONE: "12345", + QUESTION_TWO: "99999", + }, + ) as output: + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [HasExactValueRule.NOT_EXACT.format(question=QUESTION_TWO)], + ) + + +def test_changes_made_extra_commit(): + with loader.load("specs/extra_commit_on_stream1.yml", "start") as output: + assert_output(output, GitAutograderStatus.UNSUCCESSFUL, [NO_CHANGES_ERROR]) diff --git a/branch_compare/verify.py b/branch_compare/verify.py new file mode 100644 index 00000000..bf69f362 --- /dev/null +++ b/branch_compare/verify.py @@ -0,0 +1,74 @@ +from git_autograder import ( + GitAutograderBranch, + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + +from git_autograder.answers.rules import HasExactValueRule, NotEmptyRule + + +QUESTION_ONE = "Which numbers are present in stream-1 but not in stream-2?" +QUESTION_TWO = "Which numbers are present in stream-2 but not in stream-1?" +NO_CHANGES_ERROR = ( + "No changes are supposed to be made to the two branches in this exercise" +) + +FILE_PATH = "data.txt" +BRANCH_1 = "stream-1" +BRANCH_2 = "stream-2" + + +def has_made_changes(branch: GitAutograderBranch, expected_commits: int) -> bool: + """Check branch has same number of commits as expected.""" + + commits = branch.commits + return len(commits) != expected_commits + + +def get_branch_diff(exercise: GitAutograderExercise, branch1: str, branch2: str) -> str: + """Get a value present in branch1 but not in branch2.""" + exercise.repo.branches.branch(branch1).checkout() + with exercise.repo.files.file(FILE_PATH) as f1: + contents1 = f1.read() + + exercise.repo.branches.branch(branch2).checkout() + with exercise.repo.files.file(FILE_PATH) as f2: + contents2 = f2.read() + + exercise.repo.branches.branch("main").checkout() + + set1 = {line.strip() for line in contents1.splitlines() if line.strip()} + set2 = {line.strip() for line in contents2.splitlines() if line.strip()} + diff = set1 - set2 + return str(diff.pop()) + + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + branch_1 = exercise.repo.branches.branch(BRANCH_1) + branch_2 = exercise.repo.branches.branch(BRANCH_2) + if ( + not branch_1 + or not branch_2 + or has_made_changes(branch_1, 3) + or has_made_changes(branch_2, 3) + ): + raise exercise.wrong_answer([NO_CHANGES_ERROR]) + + ans_1 = get_branch_diff(exercise, BRANCH_1, BRANCH_2) + ans_2 = get_branch_diff(exercise, BRANCH_2, BRANCH_1) + + exercise.answers.add_validation( + QUESTION_ONE, + NotEmptyRule(), + HasExactValueRule(ans_1, is_case_sensitive=False), + ).add_validation( + QUESTION_TWO, + NotEmptyRule(), + HasExactValueRule(ans_2, is_case_sensitive=False), + ).validate() + + return exercise.to_output( + ["Great work comparing the branches successfully!"], + GitAutograderStatus.SUCCESSFUL, + )