Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/tests.yml
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
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest>=7.0
5 changes: 5 additions & 0 deletions tests/conftest.py
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__), "..")))
100 changes: 100 additions & 0 deletions tests/test_fibonacci.py
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
191 changes: 191 additions & 0 deletions tests/test_math_quiz.py
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 😒"
Comment thread
steam-bell-92 marked this conversation as resolved.


# ── 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)
Loading
Loading