Skip to content

Commit 79c1ba7

Browse files
Merge pull request steam-bell-92#554 from bhavyapandiya29/feature/issue-517-python-ci
feat: Add Python CI Pipeline with automated testing infrastructure
2 parents 93ecd35 + f46e0da commit 79c1ba7

7 files changed

Lines changed: 173 additions & 0 deletions

File tree

.github/workflows/python-ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Python CI Pipeline
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
python-version: ["3.10", "3.11", "3.12"]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Syntax check with py_compile
26+
run: |
27+
find . -name "*.py" -exec python -m py_compile {} +
28+
29+
- name: Run automated tests
30+
run: |
31+
python -m unittest discover -s tests -v

CONTRIBUTING.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,27 @@ These tasks are beginner-friendly and help you understand the project structure,
447447

448448
Once you feel comfortable, you can gradually try `level:intermediate`, `level:advanced`, and `level:critical` tasks.
449449

450+
## 🧪 Writing Tests
451+
452+
To maintain code quality and ensure zero external dependencies, we use the standard library's `unittest` framework.
453+
454+
1. **Test Directory**: All tests must be placed in the `tests/` directory at the root of the project.
455+
2. **File Naming**: Name your test file starting with `test_` (e.g., `test_armstrong.py`).
456+
3. **No External Modules**: Do not use external libraries like `pytest`. Stick strictly to `unittest`.
457+
4. **Mocking Inputs**: For CLI projects, use `subprocess` to provide input via `stdin`, or import helper methods directly.
458+
459+
### Running Tests Locally
460+
To run all tests and verify your changes:
461+
```bash
462+
python -m unittest discover -s tests -v
463+
```
464+
To run a syntax check across all files (similar to our CI):
465+
```bash
466+
find . -name "*.py" -exec python -m py_compile {} +
467+
```
468+
469+
---
470+
450471
## 🤔 Questions?
451472

452473
- 💬 Open an issue for questions

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[project]
2+
name = "python-mini-project"
3+
version = "0.1.0"
4+
description = "A collection of 40+ Python mini projects."
5+
requires-python = ">=3.10"
6+
7+
[tool.unittest]
8+
# No special configuration needed, unittest is standard library

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Test suite for python-mini-project

tests/test_armstrong.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import unittest
2+
import subprocess
3+
import os
4+
import sys
5+
6+
class TestArmstrongNumber(unittest.TestCase):
7+
def setUp(self):
8+
# Path to the Armstrong-Number.py script
9+
self.script_path = os.path.join("math", "Armstrong-Number", "Armstrong-Number.py")
10+
11+
def run_script_with_input(self, user_input):
12+
# Run the script as a subprocess and provide input via stdin
13+
process = subprocess.Popen(
14+
[sys.executable, self.script_path],
15+
stdin=subprocess.PIPE,
16+
stdout=subprocess.PIPE,
17+
stderr=subprocess.PIPE,
18+
text=True
19+
)
20+
stdout, stderr = process.communicate(input=user_input)
21+
return stdout, stderr
22+
23+
def test_valid_armstrong_number(self):
24+
stdout, _ = self.run_script_with_input("153\n")
25+
self.assertIn("153 is an Armstrong Number!", stdout)
26+
27+
def test_invalid_armstrong_number(self):
28+
stdout, _ = self.run_script_with_input("154\n")
29+
self.assertIn("154 is NOT an Armstrong Number.", stdout)
30+
31+
def test_invalid_input_handling(self):
32+
# The script asks again if input is invalid, so we send invalid then valid
33+
stdout, _ = self.run_script_with_input("abc\n153\n")
34+
self.assertIn("That doesn't look like a valid number", stdout)
35+
self.assertIn("153 is an Armstrong Number!", stdout)
36+
37+
def test_negative_input(self):
38+
stdout, _ = self.run_script_with_input("-5\n153\n")
39+
self.assertIn("Please enter a positive number!", stdout)
40+
41+
if __name__ == "__main__":
42+
unittest.main()

tests/test_number_guessing.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import unittest
2+
import importlib.util
3+
import os
4+
import sys
5+
6+
class TestNumberGuessing(unittest.TestCase):
7+
def setUp(self):
8+
# Load the module dynamically since it has hyphens in the name
9+
module_name = "number_guessing"
10+
file_path = os.path.join("games", "Number-Guessing-Game", "Number-Guessing-Game.py")
11+
12+
spec = importlib.util.spec_from_file_location(module_name, file_path)
13+
self.module = importlib.util.module_from_spec(spec)
14+
sys.modules[module_name] = self.module
15+
spec.loader.exec_module(self.module)
16+
17+
def test_play_round_with_guesses_win(self):
18+
# Test winning exactly on the last attempt
19+
won, attempts = self.module.play_round_with_guesses([10, 20, 50], number=50, max_attempts=3)
20+
self.assertTrue(won)
21+
self.assertEqual(attempts, 3)
22+
23+
def test_play_round_with_guesses_lose(self):
24+
# Test running out of attempts
25+
won, attempts = self.module.play_round_with_guesses([10, 20, 30], number=50, max_attempts=3)
26+
self.assertFalse(won)
27+
self.assertEqual(attempts, 3)
28+
29+
def test_play_round_with_guesses_win_early(self):
30+
# Test winning before max attempts are reached
31+
won, attempts = self.module.play_round_with_guesses([50, 60, 70], number=50, max_attempts=5)
32+
self.assertTrue(won)
33+
self.assertEqual(attempts, 1)
34+
35+
if __name__ == "__main__":
36+
unittest.main()

tests/test_typing_speed.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import unittest
2+
import subprocess
3+
import os
4+
import sys
5+
6+
class TestTypingSpeed(unittest.TestCase):
7+
def setUp(self):
8+
self.script_path = os.path.join("utilities", "Typing-Speed-Tester", "Typing-Speed-Tester.py")
9+
10+
def run_script_with_input(self, user_input):
11+
process = subprocess.Popen(
12+
[sys.executable, self.script_path],
13+
stdin=subprocess.PIPE,
14+
stdout=subprocess.PIPE,
15+
stderr=subprocess.PIPE,
16+
text=True
17+
)
18+
stdout, stderr = process.communicate(input=user_input)
19+
return stdout, stderr
20+
21+
def test_typing_speed_script_runs(self):
22+
# The script asks to press Enter to start, then waits for the typed text.
23+
# We provide a newline to start, and then a dummy text to finish.
24+
dummy_input = "\nThis is a test run.\n"
25+
stdout, _ = self.run_script_with_input(dummy_input)
26+
27+
self.assertIn("WELCOME TO TYPING SPEED TESTER", stdout)
28+
self.assertIn("TEST COMPLETED", stdout)
29+
self.assertIn("Time Taken", stdout)
30+
self.assertIn("Typing Speed", stdout)
31+
self.assertIn("Accuracy", stdout)
32+
33+
if __name__ == "__main__":
34+
unittest.main()

0 commit comments

Comments
 (0)