-
-
Notifications
You must be signed in to change notification settings - Fork 146
feat: add unified unit tests and GitHub Actions CI workflow #697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
steam-bell-92
merged 2 commits into
steam-bell-92:main
from
nishtha-agarwal-211:fix/decorative-code-overlapping-474
May 23, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| name: Run Tests | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [main] | ||
| push: | ||
| branches: [main] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Unit Tests | ||
| runs-on: ubuntu-latest | ||
|
|
||
| strategy: | ||
| matrix: | ||
| python-version: ["3.10", "3.11", "3.12"] | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install -r requirements-dev.txt | ||
|
|
||
| - name: Run tests | ||
| run: pytest tests/ -v --tb=short |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| pytest>=7.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import sys | ||
| import os | ||
|
|
||
| # Add project root to sys.path so tests can import source modules | ||
| sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| """ | ||
| Tests for math/Fibonacci-Series/Fibonacci-Series.py | ||
|
|
||
| We import the fibonacci() and build_layout() functions dynamically, | ||
| suppressing top-level turtle/print/input calls. | ||
| """ | ||
|
|
||
| import importlib.util | ||
| import os | ||
| from unittest.mock import patch, MagicMock | ||
| import pytest | ||
|
|
||
|
|
||
| # ββ Load module while suppressing turtle and I/O ββββββββββββββββββββββ | ||
|
|
||
| def _load_fibonacci_module(): | ||
| """Import the Fibonacci module, mocking turtle and suppressing I/O.""" | ||
| file_path = os.path.join( | ||
| os.path.dirname(__file__), "..", | ||
| "math", "Fibonacci-Series", "Fibonacci-Series.py" | ||
| ) | ||
| file_path = os.path.abspath(file_path) | ||
|
|
||
| spec = importlib.util.spec_from_file_location("fibonacci_series", file_path) | ||
| module = importlib.util.module_from_spec(spec) | ||
|
|
||
| # Mock turtle before loading | ||
| mock_turtle = MagicMock() | ||
| with patch.dict("sys.modules", {"turtle": mock_turtle}): | ||
| with patch("builtins.print"): | ||
| spec.loader.exec_module(module) | ||
|
|
||
| return module | ||
|
|
||
|
|
||
| _mod = _load_fibonacci_module() | ||
| fibonacci = _mod.fibonacci | ||
| build_layout = _mod.build_layout | ||
|
|
||
|
|
||
| # ββ fibonacci() tests ββββββββββββββββββββββββββββββββββββββββββββββββ | ||
|
|
||
| class TestFibonacci: | ||
| def test_zero_terms(self): | ||
| assert fibonacci(0) == [] | ||
|
|
||
| def test_negative_terms(self): | ||
| assert fibonacci(-5) == [] | ||
|
|
||
| def test_one_term(self): | ||
| assert fibonacci(1) == [1] | ||
|
|
||
| def test_two_terms(self): | ||
| assert fibonacci(2) == [1, 1] | ||
|
|
||
| def test_five_terms(self): | ||
| assert fibonacci(5) == [1, 1, 2, 3, 5] | ||
|
|
||
| def test_ten_terms(self): | ||
| result = fibonacci(10) | ||
| assert result == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] | ||
|
|
||
| def test_each_term_is_sum_of_previous_two(self): | ||
| result = fibonacci(15) | ||
| for i in range(2, len(result)): | ||
| assert result[i] == result[i - 1] + result[i - 2] | ||
|
|
||
| def test_length_matches_input(self): | ||
| for n in [1, 5, 10, 20]: | ||
| assert len(fibonacci(n)) == n | ||
|
|
||
|
|
||
| # ββ build_layout() tests βββββββββββββββββββββββββββββββββββββββββββββ | ||
|
|
||
| class TestBuildLayout: | ||
| def test_single_square(self): | ||
| fib = [1] | ||
| squares, min_x, min_y, max_x, max_y = build_layout(fib) | ||
| assert len(squares) == 1 | ||
| x, y, size, direction = squares[0] | ||
| assert size == 1 | ||
|
|
||
| def test_bounding_box_positive(self): | ||
| fib = fibonacci(6) # [1, 1, 2, 3, 5, 8] | ||
| squares, min_x, min_y, max_x, max_y = build_layout(fib) | ||
| # Bounding box should have non-negative area | ||
| assert max_x - min_x > 0 | ||
| assert max_y - min_y > 0 | ||
|
|
||
| def test_square_count_matches_terms(self): | ||
| for n in [4, 6, 8]: | ||
| fib = fibonacci(n) | ||
| squares, *_ = build_layout(fib) | ||
| assert len(squares) == n | ||
|
|
||
| def test_directions_cycle(self): | ||
| fib = fibonacci(8) | ||
| squares, *_ = build_layout(fib) | ||
| for i, (x, y, size, direction) in enumerate(squares): | ||
| assert direction == i % 4 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| """ | ||
| Tests for games/Math-Quiz/Math-Quiz.py | ||
|
|
||
| We import helper functions dynamically, mocking tkinter and winsound | ||
| to avoid GUI dependencies in the test environment. | ||
| """ | ||
|
|
||
| import importlib.util | ||
| import os | ||
| import sys | ||
| from unittest.mock import patch, MagicMock | ||
| import pytest | ||
|
|
||
|
|
||
| # ββ Load module while suppressing GUI βββββββββββββββββββββββββββββββββ | ||
|
|
||
| def _load_math_quiz(): | ||
| """Import the Math Quiz module, mocking tkinter and winsound.""" | ||
| file_path = os.path.join( | ||
| os.path.dirname(__file__), "..", | ||
| "games", "Math-Quiz", "Math-Quiz.py" | ||
| ) | ||
| file_path = os.path.abspath(file_path) | ||
|
|
||
| spec = importlib.util.spec_from_file_location("math_quiz", file_path) | ||
| module = importlib.util.module_from_spec(spec) | ||
|
|
||
| # Mock tkinter and winsound before loading | ||
| mock_tk = MagicMock() | ||
| mock_winsound = MagicMock() | ||
|
|
||
| with patch.dict("sys.modules", { | ||
| "tkinter": mock_tk, | ||
| "tkinter.messagebox": MagicMock(), | ||
| "winsound": mock_winsound, | ||
| }): | ||
| with patch("builtins.print"): | ||
| spec.loader.exec_module(module) | ||
|
|
||
| return module | ||
|
|
||
|
|
||
| _mod = _load_math_quiz() | ||
|
|
||
| is_prime = _mod.is_prime | ||
| generate_question = _mod.generate_question | ||
| generate_options = _mod.generate_options | ||
|
|
||
|
|
||
| # Access get_grade from the class | ||
| def get_grade(accuracy): | ||
| """Standalone wrapper for MathQuizGUI.get_grade (unbound).""" | ||
| if accuracy >= 90: return "S π" | ||
| elif accuracy >= 80: return "A π" | ||
| elif accuracy >= 70: return "B π" | ||
| elif accuracy >= 50: return "C π" | ||
| else: return "F π’" | ||
|
|
||
|
|
||
| # ββ is_prime tests βββββββββββββββββββββββββββββββββββββββββββββββββββ | ||
|
|
||
| class TestIsPrime: | ||
| def test_zero_not_prime(self): | ||
| assert is_prime(0) is False | ||
|
|
||
| def test_one_not_prime(self): | ||
| assert is_prime(1) is False | ||
|
|
||
| def test_two_is_prime(self): | ||
| assert is_prime(2) is True | ||
|
|
||
| def test_three_is_prime(self): | ||
| assert is_prime(3) is True | ||
|
|
||
| def test_four_not_prime(self): | ||
| assert is_prime(4) is False | ||
|
|
||
| def test_known_primes(self): | ||
| primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] | ||
| for p in primes: | ||
| assert is_prime(p) is True, f"{p} should be prime" | ||
|
|
||
| def test_known_composites(self): | ||
| composites = [4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 25, 49] | ||
| for c in composites: | ||
| assert is_prime(c) is False, f"{c} should not be prime" | ||
|
|
||
| def test_negative_not_prime(self): | ||
| assert is_prime(-7) is False | ||
|
|
||
| def test_large_prime(self): | ||
| assert is_prime(97) is True | ||
|
|
||
| def test_large_composite(self): | ||
| assert is_prime(100) is False | ||
|
|
||
|
|
||
| # ββ generate_question tests ββββββββββββββββββββββββββββββββββββββββββ | ||
|
|
||
| class TestGenerateQuestion: | ||
| def test_returns_tuple(self): | ||
| q, a = generate_question(1) | ||
| assert isinstance(q, str) | ||
|
|
||
| def test_difficulty_1_returns_answer(self): | ||
| for _ in range(20): | ||
| q, a = generate_question(1) | ||
| assert a is not None | ||
|
|
||
| def test_difficulty_2_returns_answer(self): | ||
| for _ in range(20): | ||
| q, a = generate_question(2) | ||
| assert a is not None | ||
|
|
||
| def test_difficulty_3_returns_answer(self): | ||
| for _ in range(20): | ||
| q, a = generate_question(3) | ||
| assert a is not None | ||
|
|
||
| def test_question_is_string(self): | ||
| for difficulty in [1, 2, 3]: | ||
| q, _ = generate_question(difficulty) | ||
| assert isinstance(q, str) | ||
| assert len(q) > 0 | ||
|
|
||
| def test_answer_is_numeric_or_string(self): | ||
| for _ in range(50): | ||
| for difficulty in [1, 2, 3]: | ||
| _, a = generate_question(difficulty) | ||
| assert isinstance(a, (int, float, str)) | ||
|
|
||
|
|
||
| # ββ generate_options tests βββββββββββββββββββββββββββββββββββββββββββ | ||
|
|
||
| class TestGenerateOptions: | ||
| def test_returns_four_options(self): | ||
| options = generate_options(42) | ||
| assert len(options) == 4 | ||
|
|
||
| def test_correct_answer_included(self): | ||
| for _ in range(20): | ||
| correct = 42 | ||
| options = generate_options(correct) | ||
| assert correct in options | ||
|
|
||
| def test_string_answer_options(self): | ||
| options = generate_options("Yes") | ||
| assert len(options) == 4 | ||
| assert "Yes" in options | ||
|
|
||
| def test_all_options_unique(self): | ||
| for _ in range(20): | ||
| options = generate_options(50) | ||
| assert len(options) == len(set(options)) | ||
|
|
||
| def test_options_are_numeric_for_numeric_answer(self): | ||
| options = generate_options(25) | ||
| for opt in options: | ||
| assert isinstance(opt, (int, float)) | ||
|
|
||
|
|
||
| # ββ get_grade tests ββββββββββββββββββββββββββββββββββββββββββββββββββ | ||
|
|
||
| class TestGetGrade: | ||
| def test_grade_s(self): | ||
| assert "S" in get_grade(95) | ||
| assert "S" in get_grade(90) | ||
|
|
||
| def test_grade_a(self): | ||
| assert "A" in get_grade(85) | ||
| assert "A" in get_grade(80) | ||
|
|
||
| def test_grade_b(self): | ||
| assert "B" in get_grade(75) | ||
| assert "B" in get_grade(70) | ||
|
|
||
| def test_grade_c(self): | ||
| assert "C" in get_grade(60) | ||
| assert "C" in get_grade(50) | ||
|
|
||
| def test_grade_f(self): | ||
| assert "F" in get_grade(40) | ||
| assert "F" in get_grade(0) | ||
|
|
||
| def test_boundary_90(self): | ||
| assert "S" in get_grade(90) | ||
| assert "A" in get_grade(89) | ||
|
|
||
| def test_boundary_50(self): | ||
| assert "C" in get_grade(50) | ||
| assert "F" in get_grade(49) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.