diff --git a/.gitignore b/.gitignore index 6504c80..c2790bc 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,7 @@ cython_debug/ # PyPI configuration file .pypirc + +# IDE +.vscode/ +.idea/ diff --git a/README.md b/README.md index f9240ca..160b882 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ pip install -r requirements.txt Run the main script to generate lottery draws: ```bash -python src/main.py +python main.py ``` The program will: @@ -60,9 +60,27 @@ The application uses a combination of entropy sources to generate truly random l All sources are combined, hashed, and processed to create seeds for the random number generator that selects the lottery numbers. +## Testing + +Unit tests are included to verify the functionality of all major components. + +### Running Tests + +Run the test suite with pytest: + +```bash +python -m pytest test/ +``` + +For a coverage report: + +```bash +python -m pytest --cov-report term --cov=./fdj_slayer test/ +``` + ## Configuration -Edit the constants in ``scr/constants.py`` to modify: +Edit the constants in ``src/constants.py`` to modify: | Parameter | Description | |------------------------|-------------------------------------------------------| diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..aa9b0ad --- /dev/null +++ b/conftest.py @@ -0,0 +1,7 @@ +""" +Add the parent directory to the sys.path so the tests can import the modules from the parent dir. +""" +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) diff --git a/fdj_slayer/__init__.py b/fdj_slayer/__init__.py new file mode 100644 index 0000000..552d51a --- /dev/null +++ b/fdj_slayer/__init__.py @@ -0,0 +1 @@ +# Empty file to make directory a package diff --git a/src/constants.py b/fdj_slayer/constants.py similarity index 100% rename from src/constants.py rename to fdj_slayer/constants.py diff --git a/src/draw.py b/fdj_slayer/draw.py similarity index 98% rename from src/draw.py rename to fdj_slayer/draw.py index d86d6ce..064c5f7 100644 --- a/src/draw.py +++ b/fdj_slayer/draw.py @@ -16,7 +16,7 @@ import multiprocessing import psutil from progress.bar import Bar -from constants import MAX_NUMBER, MAX_STAR, NUMBER_OF_NUMBERS, NUMBER_OF_STARS +from .constants import MAX_NUMBER, MAX_STAR, NUMBER_OF_NUMBERS, NUMBER_OF_STARS class Draw: diff --git a/src/weather.py b/fdj_slayer/weather.py similarity index 96% rename from src/weather.py rename to fdj_slayer/weather.py index 8887ec0..ef993d9 100644 --- a/src/weather.py +++ b/fdj_slayer/weather.py @@ -9,7 +9,7 @@ import openmeteo_requests import requests_cache from retry_requests import retry -from constants import OPENMETEO_API_URL, OPENMETEO_HOURLY_PARAMS +from .constants import OPENMETEO_API_URL, OPENMETEO_HOURLY_PARAMS class Weather: diff --git a/src/main.py b/main.py similarity index 87% rename from src/main.py rename to main.py index 37280ef..92bb05c 100644 --- a/src/main.py +++ b/main.py @@ -5,9 +5,9 @@ This script generates random draws for EuroMillions using various entropy source import random import numpy as _ -from draw import Draw -from weather import Weather -from constants import NUMBER_OF_DRAWS +from fdj_slayer.draw import Draw +from fdj_slayer.weather import Weather +from fdj_slayer.constants import NUMBER_OF_DRAWS def main(): diff --git a/requirements.txt b/requirements.txt index 94f212c..8f69ab6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,6 @@ psutil==7.0.0 requests-cache==1.2.1 retry-requests==2.0.0 numpy==2.2.4 +pytest==8.3.5 +pytest-cov==6.0.0 +pytest-mock==3.14.0 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..552d51a --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +# Empty file to make directory a package diff --git a/test/draw_test.py b/test/draw_test.py new file mode 100644 index 0000000..ec7929d --- /dev/null +++ b/test/draw_test.py @@ -0,0 +1,119 @@ +""" +Unit tests for the Draw class. +""" + +import unittest +from unittest.mock import patch, MagicMock +from io import StringIO +import sys +from fdj_slayer.draw import Draw +from fdj_slayer.constants import NUMBER_OF_NUMBERS, NUMBER_OF_STARS + + +class TestDraw(unittest.TestCase): + """Test suite for the Draw class""" + + def setUp(self): + """Set up test fixtures""" + self.mock_weather = MagicMock() + self.mock_weather.get_weather_entropy.return_value = "a" * 64 + self.draw = Draw(self.mock_weather) + + def test_get_static_entropy_pool(self): + """Test the static entropy pool generation""" + pool = self.draw.get_static_entropy_pool() + + self.assertIsInstance(pool, list) + self.assertIn("a" * 64, pool) + self.assertEqual(len(pool), 9) + + def test_get_dynamic_entropy_pool(self): + """Test that dynamic entropy pool returns different values""" + pool1 = self.draw.get_dynamic_entropy_pool() + pool2 = self.draw.get_dynamic_entropy_pool() + + self.assertIsInstance(pool1, list) + self.assertIsInstance(pool2, list) + self.assertEqual(len(pool1), len(pool2)) + self.assertNotEqual(pool1, pool2) + + def test_generate_seed(self): + """Test that seed generation produces an integer from entropy pool""" + base_pool = ["test1", "test2"] + seed = self.draw.generate_seed(base_pool) + + self.assertIsInstance(seed, int) + + @patch('fdj_slayer.draw.random') + def test_make_draw(self, mock_random): + """Test the draw creation with specified numbers and stars""" + mock_random.seed.return_value = None + mock_random.sample.side_effect = [ + [10, 20, 30, 40, 50], + [5, 10] + ] + + draw = self.draw.make_draw(["test_entropy"]) + + self.assertIn("seed", draw) + self.assertIn("numbers", draw) + self.assertIn("stars", draw) + self.assertEqual(len(draw["numbers"]), NUMBER_OF_NUMBERS) + self.assertEqual(len(draw["stars"]), NUMBER_OF_STARS) + self.assertEqual(draw["numbers"], [10, 20, 30, 40, 50]) + self.assertEqual(draw["stars"], [5, 10]) + + @patch('fdj_slayer.draw.Bar') + def test_generate_draws(self, mock_bar): + """Test generation of multiple draws""" + mock_bar_instance = mock_bar.return_value + + self.draw.make_draw = MagicMock( + side_effect=[{"numbers": [1, 2, 3, 4, 5], + "stars": [1, 2], "seed": i} for i in range(3)] + ) + + draws = self.draw.generate_draws(3) + + self.assertEqual(len(draws), 3) + mock_bar_instance.next.assert_called() + mock_bar_instance.finish.assert_called_once() + + def test_display_draw(self): + """Test draw display formatting""" + captured_output = StringIO() + sys.stdout = captured_output + + try: + draw = {"numbers": [1, 2, 3, 4, 5], "stars": [1, 2], "seed": 12345} + self.draw.display_draw(draw, index=0, title="TEST") + + output = captured_output.getvalue() + self.assertIn("TEST", output) + self.assertIn("[1, 2, 3, 4, 5]", output) + self.assertIn("[1, 2]", output) + finally: + sys.stdout = sys.__stdout__ + + @patch('builtins.input') + def test_display_additional_draws(self, mock_input): + """Test the additional draws display functionality""" + mock_input.side_effect = ['y', 'n'] + + self.draw.display_draw = MagicMock() + + draws = [ + {"numbers": [1, 2, 3, 4, 5], "stars": [1, 2], "seed": 1}, + {"numbers": [6, 7, 8, 9, 10], "stars": [3, 4], "seed": 2}, + {"numbers": [11, 12, 13, 14, 15], "stars": [5, 6], "seed": 3} + ] + displayed_draws = {0} + + self.draw.display_additional_draws(draws, displayed_draws) + + self.assertEqual(len(displayed_draws), 2) + self.draw.display_draw.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/test/weather_test.py b/test/weather_test.py new file mode 100644 index 0000000..a642887 --- /dev/null +++ b/test/weather_test.py @@ -0,0 +1,37 @@ +""" +Unit tests for the Weather class. +""" + +import unittest +from unittest.mock import patch +from fdj_slayer.weather import Weather + + +class TestWeather(unittest.TestCase): + """Test suite for the Weather class""" + + def setUp(self): + self.weather = Weather() + + @patch('fdj_slayer.weather.Weather._get_weather_data') + def test_get_weather_entropy_success(self, mock_get_data): + """Test the get_weather_entropy method""" + expected_hash = "a" * 64 + mock_get_data.return_value = expected_hash + + result = self.weather.get_weather_entropy() + self.assertEqual(result, expected_hash) + + @patch('fdj_slayer.weather.Weather._get_weather_data') + def test_get_weather_entropy_fallback(self, mock_get_data): + """Test the get_weather_entropy method when the API fails""" + mock_get_data.side_effect = Exception("API Error") + + result = self.weather.get_weather_entropy() + + self.assertEqual(len(result), 64) + self.assertTrue(all(c in '0123456789abcdef' for c in result)) + + +if __name__ == '__main__': + unittest.main()