diff --git a/branch_compare/.gitmastery-exercise.json b/branch_compare/.gitmastery-exercise.json new file mode 100644 index 0000000..934bbf7 --- /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 0000000..f3ac437 --- /dev/null +++ b/branch_compare/README.md @@ -0,0 +1,9 @@ +# branch-compare + +## Task + +You are recording a numerical data stream from two sources. The data are stored in `data.txt`, using a different branch for each stream. The two data streams are supposed to be identical but can vary on rare occasions. + +Answer the questions given in `answers.txt`. + +Run `gitmastery verify` to check if your answers are correct. diff --git a/branch_compare/__init__.py b/branch_compare/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/branch_compare/download.py b/branch_compare/download.py new file mode 100644 index 0000000..f0f5c12 --- /dev/null +++ b/branch_compare/download.py @@ -0,0 +1,62 @@ +from exercise_utils.cli import run_command +from exercise_utils.git import add, commit, checkout +from exercise_utils.file import append_to_file +from exercise_utils.gitmastery import create_start_tag + +import random + +__resources__ = {} + +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): + + create_start_tag(verbose) + + orig_data = get_sequence() + modified_data = get_modified_sequence(orig_data) + + run_command(["touch", "data.txt"], verbose) + add(["data.txt"], verbose) + commit("Add empty data.txt", verbose) + checkout("stream-1", True, verbose) + + for i in orig_data: + append_to_file("data.txt", str(i)+"\n") + + add(["data.txt"], verbose) + commit("Add data to data.txt", verbose) + + + checkout("main", False, verbose) + checkout("stream-2", True, verbose) + + for i in modified_data: + append_to_file("data.txt", str(i)+"\n") + + 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 0000000..7de3395 --- /dev/null +++ b/branch_compare/res/answers.txt @@ -0,0 +1,5 @@ +Q: Which number (write only one number) is present in branch stream-1 but not in branch stream-2? +A: + +Q: Which number (write only one number) is present in branch stream-2 but not in branch stream-1? +A: diff --git a/branch_compare/tests/__init__.py b/branch_compare/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/branch_compare/tests/specs/base.yml b/branch_compare/tests/specs/base.yml new file mode 100644 index 0000000..4b2a17a --- /dev/null +++ b/branch_compare/tests/specs/base.yml @@ -0,0 +1,44 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + + # Create stream-1 with data.txt containing a unique number 12345 + - 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.txt on stream-1 + + # Return to main + - type: checkout + branch-name: main + + # Create stream-2 with data.txt containing a unique number 98765 + - 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.txt on stream-2 + + # Return to main at the end for verify to start cleanly + - 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 0000000..29f2765 --- /dev/null +++ b/branch_compare/tests/specs/extra_commit_on_stream1.yml @@ -0,0 +1,55 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start + + # Base setup identical to base.yml + - 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.txt on stream-1 + + - 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.txt on stream-2 + + # Make an extra change on stream-1 to simulate user changes + - 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 + + # Return to main at the end + - 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 0000000..4d0ffa3 --- /dev/null +++ b/branch_compare/tests/test_verify.py @@ -0,0 +1,55 @@ +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 0000000..f89fce2 --- /dev/null +++ b/branch_compare/verify.py @@ -0,0 +1,83 @@ +from git_autograder import ( + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + +from git_autograder.answers.rules import HasExactValueRule, NotEmptyRule + + +QUESTION_ONE = "Which number (write only one number) is present in branch stream-1 but not in branch stream-2?" +QUESTION_TWO = "Which number (write only one number) is present in branch stream-2 but not in branch 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(exercise: GitAutograderExercise) -> bool: + repo = exercise.repo.repo + + for bname in (BRANCH_1, BRANCH_2): + if not exercise.repo.branches.has_branch(bname): + return True + + head = repo.commit(bname) + + if len(head.parents) != 1: + return True + + # Count commits unique to branch relative to main + merge_bases = repo.merge_base(bname, "main") + if not merge_bases: + return True + base = merge_bases[0] + unique_commits = list(repo.iter_commits(f"{base.hexsha}..{bname}")) + if len(unique_commits) != 1: + return True + + return False + +def get_branch_diff(exercise: GitAutograderExercise, branch1: str, branch2: str) -> str: + 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 get_stream1_diff(exercise: GitAutograderExercise) -> str: + return get_branch_diff(exercise, BRANCH_1, BRANCH_2) + +def get_stream2_diff(exercise: GitAutograderExercise) -> str: + return get_branch_diff(exercise, BRANCH_2, BRANCH_1) + +def verify(exercise: GitAutograderExercise) -> GitAutograderOutput: + + if has_made_changes(exercise): + return exercise.to_output([NO_CHANGES_ERROR], GitAutograderStatus.UNSUCCESSFUL) + + exercise.repo.branches.branch("main").checkout() + + ans_1 = get_stream1_diff(exercise) + ans_2 = get_stream2_diff(exercise) + + exercise.answers.add_validation( + QUESTION_ONE, + NotEmptyRule(), + HasExactValueRule(ans_1), + ).add_validation( + QUESTION_TWO, + NotEmptyRule(), + HasExactValueRule(ans_2), + ).validate() + + return exercise.to_output(["Great work comparing the branches successfully!"], GitAutograderStatus.SUCCESSFUL)