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..34cb99b --- /dev/null +++ b/branch_compare/res/answers.txt @@ -0,0 +1,5 @@ +Q: Which number is present in branch stream-1 but not in branch stream-2? +A: + +Q: Which 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..00c3a53 --- /dev/null +++ b/branch_compare/tests/specs/base.yml @@ -0,0 +1,6 @@ +initialization: + steps: + - type: commit + empty: true + message: Empty commit + id: start diff --git a/branch_compare/tests/test_verify.py b/branch_compare/tests/test_verify.py new file mode 100644 index 0000000..5076c30 --- /dev/null +++ b/branch_compare/tests/test_verify.py @@ -0,0 +1,66 @@ +from git_autograder import GitAutograderStatus, GitAutograderTestLoader, assert_output +from unittest.mock import patch +from git_autograder.answers.rules.has_exact_value_rule import HasExactValueRule + +from ..verify import verify, QUESTION_ONE, QUESTION_TWO + +REPOSITORY_NAME = "branch-compare" + +loader = GitAutograderTestLoader(__file__, REPOSITORY_NAME, verify) + + +def test_base(): + with ( + patch("branch_compare.verify.has_made_changes", return_value=False), + patch("branch_compare.verify.get_stream1_diff", return_value="12345"), + patch("branch_compare.verify.get_stream2_diff", return_value="98765"), + 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 ( + patch("branch_compare.verify.has_made_changes", return_value=False), + patch("branch_compare.verify.get_stream1_diff", return_value="99999"), + patch("branch_compare.verify.get_stream2_diff", return_value="98765"), + loader.load( + "specs/base.yml", + "start", + mock_answers={ + QUESTION_ONE: "12345", + QUESTION_TWO: "98765", + } + ) as output, + ): + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [HasExactValueRule.NOT_EXACT.format(question=QUESTION_ONE)], + ) + +def test_wrong_stream2_diff(): + with ( + patch("branch_compare.verify.has_made_changes", return_value=False), + patch("branch_compare.verify.get_stream1_diff", return_value="12345"), + patch("branch_compare.verify.get_stream2_diff", return_value="99999"), + loader.load( + "specs/base.yml", + "start", + mock_answers={ + QUESTION_ONE: "12345", + QUESTION_TWO: "98765", + } + ) as output, + ): + assert_output( + output, + GitAutograderStatus.UNSUCCESSFUL, + [HasExactValueRule.NOT_EXACT.format(question=QUESTION_TWO)], + ) diff --git a/branch_compare/verify.py b/branch_compare/verify.py new file mode 100644 index 0000000..3815bd5 --- /dev/null +++ b/branch_compare/verify.py @@ -0,0 +1,72 @@ +from git_autograder import ( + GitAutograderOutput, + GitAutograderExercise, + GitAutograderStatus, +) + +from git_autograder.answers.rules import HasExactValueRule, NotEmptyRule + + +QUESTION_ONE = "Which number is present in branch stream-1 but not in branch stream-2?" +QUESTION_TWO = "Which number is present in branch stream-2 but not in branch stream-1?" + +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 + + 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 are supposed to be made to the two branches in this exercise"], 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)