diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..097b4ab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.pythonPath": "/usr/local/bin/python3", + "python.testing.unittestArgs": ["-v", "-s", ".", "-p", "test*.py"], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": ["."] +} diff --git a/README.md b/README.md index e7a2f34..678da2f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,105 @@ -# PythonCodeExecises -Test and creating python samples to do Katas with and to keep. readme of good practises +# Introduction + +Create simple unit testing sample and understand good naming and generation of samples ... + +The inspiration for this is taken from this article https://testdriven.io/blog/modern-tdd/ + +# Setup + +### Using poetry (latest and greatest) + +1. Detailed information can be found at https://python-poetry.org/docs/ + +2. Install`curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -` + +3. Alternative install can be done through **pip** `pip install --user poetry` or **pipx** `pipx install poetry` + +4. Once installed this will allow you to check the version installed running `poetry --version` + +5. Update poetry `poetry self update` + +6. [Basic usage](https://python-poetry.org/docs/basic-usage/) of setting up a project can be done + + ```bash + cd pre-existing-project + poetry init + ``` + +7. To install all dependencies `poetry install` + +8. To understand what poetry does use `poetry -h` + +9. To run tests outside of vscode test runner `poetry run pytest` but configure the vscode test explorer for better results + + ![image-20220328211508369](./vscode-test-explorer.png) + +### Using a virtual environment + +1. Setup the version of python to work with: + + ```bash + python3.10 -m venv env + source env/bin/activate + pip install -U pip + ``` + +1. Install dependencies `pip install -r requirements.txt` + +### Using the library path (beginner stuff really) + +1. Install `pip install -U pytest` + +1. To figure out which components to upgrade run `pip list -o` + +1. [pytest](https://docs.pytest.org/en/stable/) is the test framework used by the article + +1. Documentation can be found https://docs.pytest.org/en/stable/getting-started.html + +1. Running the tests through python I setup `python3 -m pytest `. **NOTE:** the test name always needs to start with **test\_** or end with **\_test** or the test discovery will not happen + +1. VSCode Tips and tricks: + + - With **vscode**, I needed to setup my interpreter path to utilise Python 3.9.1 for everythign to work with the correct version of Python. This can be found on the bottom left panel of your IDE footer + + - Setting up **text explorer**, add the extension python test explorer and then see this [https://code.visualstudio.com/docs/python/testing](https://code.visualstudio.com/docs/python/testing) for more information + + - Configure settings with [Pytest](https://docs.pytest.org/en/stable/contents.html) + + ```json + { + "python.pythonPath": "/opt/local/bin/python3", + "python.testing.pytestArgs": ["tests"], + "python.testing.unittestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.autoTestDiscoverOnSaveEnabled": true + } + ``` + +1. Install pip with python 3 or upgrade pip to the latest + + ```shell + python3 -m pip install + python3 -m pip install --upgrade pip + ``` + +1. Create a virtual environment using ` python3 -m venv ./opt/local` + +1. Setting up a project structure should resemble https://docs.python-guide.org/writing/structure/ + +1. Make sure if you have folder that you always include `__init__.py`files which can include paths, setting variables or just logging - see more [here](https://www.datacamp.com/community/tutorials/role-underscore-python) + +1. **Pytest** install all things needed: + + ``` + pip install pytest + pip install pytest-sugar + pip install pytest-cov + ``` + + - `pytest --fixtures` gives a list of fixtures, including one built up with setup fixtures that may have doc comments + - `pytest --markers` gives a list of markers to help decorate functions with + - `pip install pytest-html` and output html to show reports and output an html report `pytest --html=report.html` + - **Mocking** details can be here within [https://docs.python.org/3/library/unittest.mock.html#module-unittest.mock](https://docs.python.org/3/library/unittest.mock.html#module-unittest.mock) + - `pytest --cov-report html:cov_html --cov-branch --cov= . ` for getting coverage of a specific module being tested + - `pytest --cov=myproj tests/` should do a report for everything but it doesnt work for me at the moment diff --git a/TODO b/TODO new file mode 100644 index 0000000..4061773 --- /dev/null +++ b/TODO @@ -0,0 +1 @@ +☐ Follow test instructions and setup testing as per the document @today \ No newline at end of file diff --git a/project_example/.idea/.gitignore b/project_example/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/project_example/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/project_example/.idea/dataSources.xml b/project_example/.idea/dataSources.xml new file mode 100644 index 0000000..d90aeeb --- /dev/null +++ b/project_example/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/.coverage + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/project_example/.idea/inspectionProfiles/profiles_settings.xml b/project_example/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/project_example/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/project_example/.idea/misc.xml b/project_example/.idea/misc.xml new file mode 100644 index 0000000..e2aebb0 --- /dev/null +++ b/project_example/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/project_example/.idea/modules.xml b/project_example/.idea/modules.xml new file mode 100644 index 0000000..481305f --- /dev/null +++ b/project_example/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/project_example/.idea/project_example.iml b/project_example/.idea/project_example.iml new file mode 100644 index 0000000..a7d09ba --- /dev/null +++ b/project_example/.idea/project_example.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/project_example/.idea/vcs.xml b/project_example/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/project_example/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/project_example/.vscode/launch.json b/project_example/.vscode/launch.json new file mode 100644 index 0000000..0f0eee6 --- /dev/null +++ b/project_example/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "Debug Tests", + "type": "python", + "request": "test", + "console": "integratedTerminal", + "justMyCode": false + } + ] +} \ No newline at end of file diff --git a/project_example/.vscode/settings.json b/project_example/.vscode/settings.json new file mode 100644 index 0000000..e6a24c9 --- /dev/null +++ b/project_example/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "python.pythonPath": "/usr/local/bin/python3", + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.autoTestDiscoverOnSaveEnabled": true +} \ No newline at end of file diff --git a/project_example/arithmetic/__init__.py b/project_example/arithmetic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project_example/arithmetic/another_sum.py b/project_example/arithmetic/another_sum.py new file mode 100644 index 0000000..04b7881 --- /dev/null +++ b/project_example/arithmetic/another_sum.py @@ -0,0 +1,2 @@ +def another_sum(a: int, b: int) -> int: + return a + b diff --git a/project_example/contacts/__init__.py b/project_example/contacts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project_example/contacts/phonebook.py b/project_example/contacts/phonebook.py new file mode 100644 index 0000000..902cf9a --- /dev/null +++ b/project_example/contacts/phonebook.py @@ -0,0 +1,14 @@ +class PhoneBook: + + def __init__(self): + self.phone_numbers = {} + + def add(self, name: str, phone_no: any): + self.phone_numbers[name.strip()] = phone_no + + def lookup(self, name: str): + return self.phone_numbers[name] + + def is_consistent(self): + unique_values = set(self.phone_numbers.values()); + return len(self.phone_numbers) == len(unique_values); diff --git a/project_example/decorators/__init__.py b/project_example/decorators/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/decorators/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/decorators/printer.py b/project_example/decorators/printer.py new file mode 100644 index 0000000..6ea9196 --- /dev/null +++ b/project_example/decorators/printer.py @@ -0,0 +1,36 @@ +import functools +def my_decorator(arg): + def inner_decorator(func): + def wrapped(*args, **kwargs): + print('before function') + response = func(*args, **kwargs) + print('after function') + return response + print('decorating', func, 'with argument', arg) + return wrapped + return inner_decorator + + +@my_decorator('foo') +def printer(a, b): + print('in function') + return a + b + + +def shared_task(bind=False, priority=1, queue='long-running'): + def decorator_shared_task(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + print('Before running task', args, kwargs, bind, priority, queue) + result = func(*args, **kwargs) + print('After running task', result) + return result + return wrapper + return decorator_shared_task + + + +@shared_task(bind=True, priority=9, queue='short-running') +def handler(a, b): + print('Running task', a, b) + return a + b diff --git a/project_example/game_of_life/__init__.py b/project_example/game_of_life/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/game_of_life/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/game_of_life/cell.py b/project_example/game_of_life/cell.py new file mode 100644 index 0000000..a17eb34 --- /dev/null +++ b/project_example/game_of_life/cell.py @@ -0,0 +1,36 @@ +from game_of_life.cell_state import CellState + + +class Cell: + + def __init__(self, current_state=CellState.Dead): + self.current_state = current_state + self.neighbours = list() + + def next_state(self) -> CellState: + def is_overpopulated(): + return alive_cell_count > 3 + + def is_underpopulated(): + return alive_cell_count < 2 + + def is_thriving(): + return self.current_state is CellState.Alive and alive_cell_count == 2 + + def is_fertile(): + return self.current_state is CellState.Dead and alive_cell_count == 3 + + next_state = self.current_state + alive_cell_count = len([neighbour for neighbour in self.neighbours if neighbour.current_state is CellState.Alive]) + if is_thriving() or is_fertile(): + next_state = CellState.Alive + if is_underpopulated() or is_overpopulated(): + next_state = CellState.Dead + return next_state + + def add_neighbours(self, neighbours: list): + for neighbour in neighbours: + self.neighbours.append(neighbour) + + def __str__(self): + return 'X' if self.current_state is CellState.Alive else ' ' diff --git a/project_example/game_of_life/cell_state.py b/project_example/game_of_life/cell_state.py new file mode 100644 index 0000000..29fe126 --- /dev/null +++ b/project_example/game_of_life/cell_state.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class CellState(Enum): + Dead = 0 + Alive = 1 diff --git a/project_example/game_of_life/generator.py b/project_example/game_of_life/generator.py new file mode 100644 index 0000000..b4d74cb --- /dev/null +++ b/project_example/game_of_life/generator.py @@ -0,0 +1,73 @@ +from game_of_life.cell import Cell +from game_of_life.cell_state import CellState +from game_of_life.position import Position +from game_of_life.string_builder import StringBuilder + + +class Generator: + + def __init__(self, size: int, seed_data: list = list()): + if size < 2: + raise ValueError('"size" must must be no less than 2 for life to exist', size) + self.size = size + self.board = [[Cell() for _ in range(self.size)] for _ in range(self.size)] + self.next_states = [[CellState.Dead] * self.size for _ in range(self.size)] + self._setup_neighbours() + self._seed(seed_data) + + def tick(self): + self._calculate_life_expectancy() + self._regenerate() + + def cell(self, x: int, y: int) -> Cell: + return self.board[y][x] + + def is_on_board(self, x: int, y: int) -> bool: + return 0 <= x < self.size and 0 <= y < self.size + + def board_positions(self) -> list[Position]: + positions = list() + for y in range(self.size): + for x in range(self.size): + positions.append(Position(x, y)) + return positions + + def __str__(self) -> str: + result = StringBuilder(' | ') + for y in range(self.size): + if y != 0: + result.newline().add(' | ') + for x in range(self.size): + result.add(f'{self.cell(x, y)} | ') + return result.to_string() + + def _regenerate(self): + for pos in self.board_positions(): + self.cell(pos.x, pos.y).current_state = self.next_states[pos.y][pos.x] + + def _calculate_life_expectancy(self): + for pos in self.board_positions(): + self.next_states[pos.y][pos.x] = self.cell(pos.x, pos.y).next_state() + + def _setup_neighbours(self): + for pos in self.board_positions(): + self.cell(pos.x, pos.y).add_neighbours(self._neighbours_by_position(pos.x, pos.y)) + + def _seed(self, positions: list[(int, int)]): + for item in positions: + x = item[0] + y = item[1] + if not self.is_on_board(x, y): + message = f"[{x}, {y}] should have values in the range of 0 - {self.size - 1}" + raise ValueError(message, x, y) + self.cell(x, y).current_state = CellState.Alive + + def _neighbours_by_position(self, x: int, y: int) -> list[Cell]: + y_range = [y - 1, y, y + 1] + x_range = [x - 1, x, x + 1] + neighbours = list() + for row in y_range: + for col in x_range: + if (x != col or y != row) and self.is_on_board(col, row): + neighbours.append(self.cell(col, row)) + return neighbours diff --git a/project_example/game_of_life/position.py b/project_example/game_of_life/position.py new file mode 100644 index 0000000..cb27f96 --- /dev/null +++ b/project_example/game_of_life/position.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class Position: + x: int + y: int diff --git a/project_example/game_of_life/readme.md b/project_example/game_of_life/readme.md new file mode 100644 index 0000000..24a55bf --- /dev/null +++ b/project_example/game_of_life/readme.md @@ -0,0 +1,14 @@ +The universe of the Game of Life is an infinite, two-dimensional [orthogonal](https://en.wikipedia.org/wiki/Orthogonality) grid of square *cells*, each of which is in one of two possible states, *live* or *dead*, (or *populated* and *unpopulated*, respectively). Every cell interacts with its eight *[neighbours](https://en.wikipedia.org/wiki/Moore_neighborhood)*, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur: + +1. Any live cell with fewer than two live neighbours dies, as if by underpopulation. +2. Any live cell with two or three live neighbours lives on to the next generation. +3. Any live cell with more than three live neighbours dies, as if by overpopulation. +4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. + +These rules, which compare the behavior of the automaton to real life, can be condensed into the following: + +1. Any live cell with two or three live neighbours survives. +2. Any dead cell with three live neighbours becomes a live cell. +3. All other live cells die in the next generation. Similarly, all other dead cells stay dead. + +The initial pattern constitutes the **seed** of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed, live or dead; births and deaths occur simultaneously, and the discrete moment at which this happens is sometimes called a *tick*. Each generation is a *[pure function](https://en.wikipedia.org/wiki/Pure_function)* of the preceding one. The rules continue to be applied repeatedly to create further generations. \ No newline at end of file diff --git a/project_example/game_of_life/string_builder.py b/project_example/game_of_life/string_builder.py new file mode 100644 index 0000000..c61cba5 --- /dev/null +++ b/project_example/game_of_life/string_builder.py @@ -0,0 +1,26 @@ +from os import linesep as eol + +from io import StringIO + + +class StringBuilder: + _logger = None + + def __init__(self, value=None): + self._logger = StringIO() + if value is not None: + self.add(value) + + def add(self, value): + self._logger.write(value) + return self + + def newline(self): + self.add(eol) + return self + + def to_string(self): + return str(self) + + def __str__(self): + return self._logger.getvalue() diff --git a/project_example/health/__init__.py b/project_example/health/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/health/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/health/prescription.py b/project_example/health/prescription.py new file mode 100644 index 0000000..30d33f2 --- /dev/null +++ b/project_example/health/prescription.py @@ -0,0 +1,16 @@ +from datetime import date, timedelta +from typing import Generator + +class Prescription: + def __init__(self, description: str, dispenseDate: date, daysSupply: int): + self.description = description + self.dispenseDate = dispenseDate + self.daysSupply = daysSupply + + def days_taken(self) -> Generator: + all_days = (self.dispenseDate + timedelta(days=i) + for i in range(self.daysSupply)) + return (day for day in all_days if day < date.today()) + + def to_string(self): + return f"{self.description} should be dispensed on the '{self.dispenseDate}' with only {self.daysSupply} days supply" diff --git a/project_example/main.py b/project_example/main.py new file mode 100644 index 0000000..ca215d4 --- /dev/null +++ b/project_example/main.py @@ -0,0 +1,19 @@ +from arithmetic.another_sum import another_sum +from requests import get + +def getNumbers(): + first_number = int(input('What is the first number? ')) + second_number = int(input('What is the second number? ')) + print(another_sum(first_number, second_number)) + +def getConfirmation(): + global confirmation + confirmation = input('Would you like to do some sums? (Y or n) ') + +confirmation = 'Y' +while confirmation.strip().upper() == 'Y': + getNumbers() + getConfirmation() + +response = get('https://httpbin.org/ip') +print('Your IP is {0}'.format(response.json()['origin'])) diff --git a/project_example/poetry.lock b/project_example/poetry.lock new file mode 100644 index 0000000..1eb3538 --- /dev/null +++ b/project_example/poetry.lock @@ -0,0 +1,233 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyparsing" +version = "3.0.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "e2c388391266f5bbc7b9bed6540e86f645b97892a5f362f09093e7f3562a83e8" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyparsing = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] diff --git a/project_example/pyproject.toml b/project_example/pyproject.toml new file mode 100644 index 0000000..4f5d3cc --- /dev/null +++ b/project_example/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "project_example" +version = "0.1.0" +description = "Test Python ideas" +authors = ["Vincent Farah "] + +[tool.poetry.dependencies] +python = "^3.10" +requests = "^2.25.1" + +[tool.poetry.dev-dependencies] +pytest = "^6.2.5" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/project_example/setup.py b/project_example/setup.py new file mode 100644 index 0000000..c994b1c --- /dev/null +++ b/project_example/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='project_example', + version='', + packages=['sso', 'health', 'contacts', 'utilities', 'arithmetic', 'tire_pressure'], + url='', + license='', + author='farahvi', + author_email='', + description='Python newb testing' +) diff --git a/project_example/sso/__init__.py b/project_example/sso/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/sso/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/sso/my_service.py b/project_example/sso/my_service.py new file mode 100644 index 0000000..64c8755 --- /dev/null +++ b/project_example/sso/my_service.py @@ -0,0 +1,13 @@ +from sso.single_sign_on import SingleSignOnRegistry +from sso.request import Request +from sso.sso_token import SSOToken +from sso.response import Response + + +class MyService: + def __init__(self, sso_registry: SingleSignOnRegistry): + self.sso_registry = sso_registry + + def handle(self, request: Request, sso_token: SSOToken = None): + result = Response("Hello {}".format(request.name)) if self.sso_registry.is_valid(sso_token) else Response("Please sign in") + return result diff --git a/project_example/sso/request.py b/project_example/sso/request.py new file mode 100644 index 0000000..4fc79f6 --- /dev/null +++ b/project_example/sso/request.py @@ -0,0 +1,3 @@ +class Request: + def __init__(self, name): + self.name = name \ No newline at end of file diff --git a/project_example/sso/response.py b/project_example/sso/response.py new file mode 100644 index 0000000..073040d --- /dev/null +++ b/project_example/sso/response.py @@ -0,0 +1,3 @@ +class Response: + def __init__(self, text): + self.text = text \ No newline at end of file diff --git a/project_example/sso/single_sign_on.py b/project_example/sso/single_sign_on.py new file mode 100644 index 0000000..1469b2a --- /dev/null +++ b/project_example/sso/single_sign_on.py @@ -0,0 +1,12 @@ +# REMARKS: Intentionally left empty to mock +class SingleSignOnRegistry: + def __init__(self): + pass + + def register_new_session(self, credentials): + """Returns an instance of an SSOToken if the credentials are valid""" + pass + + def is_valid(self, token): + """Returns True if the token refers to a current session""" + pass diff --git a/project_example/sso/sso_token.py b/project_example/sso/sso_token.py new file mode 100644 index 0000000..33cd2c0 --- /dev/null +++ b/project_example/sso/sso_token.py @@ -0,0 +1,12 @@ +import random + + +class SSOToken: + def __init__(self): + self.id = random.randrange(1_00_000) + + def __eq__(self, o: object) -> bool: + return self.id == o.id + + def __repr__(self) -> str: + return str(self.id) diff --git a/project_example/tennis/scoring.py b/project_example/tennis/scoring.py new file mode 100644 index 0000000..50360a3 --- /dev/null +++ b/project_example/tennis/scoring.py @@ -0,0 +1,6 @@ +def tennis_score(player_one_score: int, player_two_score: int): + score_names = ["Love", "Fifteen", "Thirty", "Forty"] + if player_one_score == player_two_score: + return score_names[player_one_score] + "-All" + else: + return f"{score_names[player_one_score]}-{score_names[player_two_score]}" diff --git a/project_example/tests/__init__.py b/project_example/tests/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/tests/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/tests/arithmetic/__init__.py b/project_example/tests/arithmetic/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/tests/arithmetic/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/tests/arithmetic/test_another_sum_pytest.py b/project_example/tests/arithmetic/test_another_sum_pytest.py new file mode 100644 index 0000000..f0ed70d --- /dev/null +++ b/project_example/tests/arithmetic/test_another_sum_pytest.py @@ -0,0 +1,5 @@ +from arithmetic.another_sum import another_sum + + +def test_add_two_values(): + assert another_sum(3, 2) == 5 diff --git a/project_example/tests/arithmetic/test_another_sum_unittest.py b/project_example/tests/arithmetic/test_another_sum_unittest.py new file mode 100644 index 0000000..168d8d5 --- /dev/null +++ b/project_example/tests/arithmetic/test_another_sum_unittest.py @@ -0,0 +1,7 @@ +from arithmetic.another_sum import another_sum +from unittest.case import TestCase + + +class AnotherSumShould(TestCase): + def test_add_two_values(self): + self.assertEqual(another_sum(3, 5), 8) diff --git a/project_example/tests/contacts/__init__.py b/project_example/tests/contacts/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/tests/contacts/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/tests/contacts/conftest.py b/project_example/tests/contacts/conftest.py new file mode 100644 index 0000000..af74610 --- /dev/null +++ b/project_example/tests/contacts/conftest.py @@ -0,0 +1,9 @@ +from pytest import fixture +from contacts.phonebook import PhoneBook + + +@fixture +def phonebook(): + # Fixture document comment + """Provides an empty phonebook""" + return PhoneBook() diff --git a/project_example/tests/contacts/test_phonebook_pytest.py b/project_example/tests/contacts/test_phonebook_pytest.py new file mode 100644 index 0000000..fab0680 --- /dev/null +++ b/project_example/tests/contacts/test_phonebook_pytest.py @@ -0,0 +1,54 @@ +from pytest import raises, fixture +import pytest +from contacts.phonebook import PhoneBook + + +# Moved to conftest in the folder to share but can be used here +# @fixture +# def phonebook(): +# # Fixture document comment +# "Provides an empty phonebook" +# return PhoneBook() + +@pytest.mark.slow +def test_add_creates_a_phone_book_entry(phonebook): + phonebook.add("Bob", "1234") + + assert len(phonebook.phone_numbers) == 1 + assert phonebook.phone_numbers == {"Bob": "1234"} + + +def test_add_number_phone_book_entry(phonebook): + phonebook.add("Bob", 1234) + + assert len(phonebook.phone_numbers) == 1 + assert phonebook.phone_numbers == {"Bob": 1234} + + +def test_lookup_by_name(phonebook): + phonebook.add("Bob", 1234) + + actual = phonebook.lookup("Bob") + + assert actual == 1234 + + +def test_raises_key_error_when_name_not_found(phonebook): + with raises(KeyError): + phonebook.lookup("UserThatDoesNotExist") + + +def test_is_consistent_when_there_are_no_duplicates(phonebook): + phonebook.add("Bob", 12345) + assert phonebook.is_consistent() is True + + phonebook.add("Sue", 23456) + assert phonebook.is_consistent() is True + + +def test_is_not_consistent_when_there_are_duplicate_numbers(phonebook): + phonebook.add("Bob", 12345) + assert phonebook.is_consistent() is True + + phonebook.add("Sue", 12345) + assert phonebook.is_consistent() is False diff --git a/project_example/tests/contacts/test_phonebook_unittest.py b/project_example/tests/contacts/test_phonebook_unittest.py new file mode 100644 index 0000000..6d9baf4 --- /dev/null +++ b/project_example/tests/contacts/test_phonebook_unittest.py @@ -0,0 +1,54 @@ +from contacts.phonebook import PhoneBook +from unittest import TestCase + + +class PhoneBookTest(TestCase): + + def setUp(self) -> None: + self.phonebook = PhoneBook() + + # Example of where you clean up resource + def tearDown(self) -> None: + return super().tearDown() + + def setUpPhoneBookWith(self, name: str, phone_no): + self.phonebook.add(name, phone_no) + + def test_add_creates_a_phone_book_entry(self): + self.setUpPhoneBookWith("Bob", "1234") + + self.assertEqual(len(self.phonebook.phone_numbers), 1) + self.assertDictEqual(self.phonebook.phone_numbers, {"Bob": "1234"}) + + def test_add_number_phone_book_entry(self): + self.setUpPhoneBookWith("Bob", 1234) + + self.assertEqual(len(self.phonebook.phone_numbers), 1) + self.assertDictEqual(self.phonebook.phone_numbers, {"Bob": 1234}) + + def test_lookup_by_name(self): + self.setUpPhoneBookWith("Bob", "12345") + + number = self.phonebook.lookup("Bob") + + self.assertEqual("12345", number) + + def test_raises_key_error_when_name_not_found(self): + with self.assertRaises(KeyError): + self.phonebook.lookup("UserThatDoesNotExist") + + # @unittest.skip("Showcase the skip mechanism") + def test_is_consistent_when_there_are_no_duplicates(self): + self.setUpPhoneBookWith(name="Bob", phone_no="12345") + + self.setUpPhoneBookWith("Sue", "23456") + + self.assertTrue(self.phonebook.is_consistent()) + + def test_is_not_consistent_when_there_are_duplicate_values(self): + self.setUpPhoneBookWith(name="Bob", phone_no="12345") + self.assertTrue(self.phonebook.is_consistent()) + + self.setUpPhoneBookWith("Jane", "12345") + + self.assertFalse(self.phonebook.is_consistent()) diff --git a/project_example/tests/decorators/test_printers_should.py b/project_example/tests/decorators/test_printers_should.py new file mode 100644 index 0000000..850d5dc --- /dev/null +++ b/project_example/tests/decorators/test_printers_should.py @@ -0,0 +1,24 @@ +import io +import sys + +from decorators.printer import printer, handler + + +def test_passing_arguments_to_decorators(): + actualOutput = io.StringIO() # Create StringIO object + sys.stdout = actualOutput + + actual = printer(1, 2) + + assert actualOutput.getvalue() == 'before function\nin function\nafter function\n' + assert actual == 3 + + +def test_task_execution_of_handler(): + actualOutput = io.StringIO() # Create StringIO object + sys.stdout = actualOutput + + actual = handler(1, 2) + + assert actualOutput.getvalue() == "Before running task (1, 2) {} True 9 short-running\nRunning task 1 2\nAfter running task 3\n" + assert actual == 3 diff --git a/project_example/tests/game_of_life/__init__.py b/project_example/tests/game_of_life/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/tests/game_of_life/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/tests/game_of_life/conftest.py b/project_example/tests/game_of_life/conftest.py new file mode 100644 index 0000000..91a00a3 --- /dev/null +++ b/project_example/tests/game_of_life/conftest.py @@ -0,0 +1,14 @@ +from pytest import fixture + +from game_of_life.cell import Cell +from game_of_life.cell_state import CellState + + +@fixture +def living_cell() -> Cell: + return Cell(CellState.Alive) + + +@fixture +def dead_cell() -> Cell: + return Cell() diff --git a/project_example/tests/game_of_life/test_cell_should.py b/project_example/tests/game_of_life/test_cell_should.py new file mode 100644 index 0000000..e07861b --- /dev/null +++ b/project_example/tests/game_of_life/test_cell_should.py @@ -0,0 +1,63 @@ +from game_of_life.cell import Cell +from game_of_life.cell_state import CellState + + +class TestCellShould: + + def test_initialise_by_default_as_a_dead(self): + cell = Cell() + + assert cell is not None + assert cell.current_state is CellState.Dead + + def test_come_to_life_when_three_live_neighbours_cause_reproduction(self, dead_cell): + dead_cell.add_neighbours( + [ + Cell(CellState.Alive), + Cell(CellState.Alive), + Cell(CellState.Alive), + ] + ) + + assert dead_cell.next_state() is CellState.Alive + + def test_killing_live_cell_when_fewer_than_two_live_neighbours_cause_underpopulation(self, living_cell): + living_cell.add_neighbours( + [ + Cell(CellState.Dead), + Cell(CellState.Dead), + Cell(CellState.Dead), + ] + ) + + assert living_cell.next_state() is CellState.Dead + + def test_live_cell_with_two_live_neighbours_stays_alive_by_thriving_in_ideal_conditions(self, living_cell): + living_cell.add_neighbours( + [ + Cell(CellState.Alive), + Cell(CellState.Alive), + Cell(CellState.Dead), + ] + ) + + assert living_cell.next_state() is CellState.Alive + + def test_more_than_three_live_neighbours_kills_live_cell_by_virtue_of_over_population(self, living_cell): + living_cell.add_neighbours( + [ + Cell(CellState.Alive), + Cell(CellState.Alive), + Cell(CellState.Alive), + Cell(CellState.Alive), + ] + ) + + assert living_cell.next_state() is CellState.Dead + + def test_should_visualise_cell_by_status(self, living_cell): + + assert str(living_cell) == 'X' + + living_cell.current_state = CellState.Dead + assert str(living_cell) == ' ' diff --git a/project_example/tests/game_of_life/test_generator_should.py b/project_example/tests/game_of_life/test_generator_should.py new file mode 100644 index 0000000..945f858 --- /dev/null +++ b/project_example/tests/game_of_life/test_generator_should.py @@ -0,0 +1,168 @@ +from _pytest.python_api import raises +from os import linesep as eol + +from game_of_life.generator import Generator + + +class TestGeneratorShould: + def test_a_five_by_five_empty_board(self): + generator = Generator(5) + + assert str(generator) == ( + f" | | | | | | {eol}" + f" | | | | | | {eol}" + f" | | | | | | {eol}" + f" | | | | | | {eol}" + f" | | | | | | " + ) + + def test_raise_a_value_error_when_seed_outside_range(self): + with raises(ValueError): + Generator(2, [(0, 2)]) + + def test_a_five_by_five_with_seeded_data(self): + generator = Generator(5, [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), + (0, 1) + ]) + + assert str(generator) == ( + f" | X | X | X | X | X | {eol}" + f" | X | | | | | {eol}" + f" | | | | | | {eol}" + f" | | | | | | {eol}" + f" | | | | | | " + ) + + def test_one_by_one_throws_an_value_error(self): + with raises(ValueError) as error: + Generator(1, []) + assert str(error) == ( + '" + ) + + def test_one_neighbour_dies_by_solitude(self): + generator = Generator(2, [ + (0, 0), (1, 1), + ]) + assert str(generator) == ( + f" | X | | {eol}" + f" | | X | " + ) + + generator.tick() + + assert str(generator) == ( + f" | | | {eol}" + f" | | | " + ) + + def test_two_or_three_neighbours_survive(self): + generator = Generator(3, [ + (0, 0), (1, 1), (1, 2) + ]) + assert str(generator) == ( + f" | X | | | {eol}" + f" | | X | | {eol}" + f" | | X | | " + ) + + generator.tick() + + assert str(generator) == ( + f" | | | | {eol}" + f" | X | X | | {eol}" + f" | | | | " + ) + + def test_each_cell_with_three_neighbours_becomes_populated(self): + generator = Generator(3, [ + (0, 0), (0, 1), (2, 2) + ]) + assert str(generator) == ( + f" | X | | | {eol}" + f" | X | | | {eol}" + f" | | | X | " + ) + + generator.tick() + + assert str(generator) == ( + f" | | | | {eol}" + f" | | X | | {eol}" + f" | | | | " + ) + + def test_a_four_by_four_block_still_life(self): + generator = Generator(4, [ + (1, 1), (2, 1), + (1, 2), (2, 2), + ]) + assert str(generator) == ( + f" | | | | | {eol}" + f" | | X | X | | {eol}" + f" | | X | X | | {eol}" + f" | | | | | " + ) + + generator.tick() + + assert str(generator) == ( + f" | | | | | {eol}" + f" | | X | X | | {eol}" + f" | | X | X | | {eol}" + f" | | | | | " + ) + + def test_a_three_by_three_blinker_oscillator(self): + generator = Generator(3, [ + (0, 1), (1, 1), (2, 1), + ]) + flip = ( + f" | | | | {eol}" + f" | X | X | X | {eol}" + f" | | | | " + ) + flop = ( + f" | | X | | {eol}" + f" | | X | | {eol}" + f" | | X | | " + ) + assert str(generator) == flip + + generator.tick() + assert str(generator) == flop + + generator.tick() + assert str(generator) == flip + + def test_a_six_by_six_beacon_oscillator(self): + generator = Generator(6, [ + (1, 1), (2, 1), + (1, 2), (2, 2), + (3, 3), (4, 3), + (3, 4), (4, 4), + ]) + flip = ( + f" | | | | | | | {eol}" + f" | | X | X | | | | {eol}" + f" | | X | X | | | | {eol}" + f" | | | | X | X | | {eol}" + f" | | | | X | X | | {eol}" + f" | | | | | | | " + ) + flop = ( + f" | | | | | | | {eol}" + f" | | X | X | | | | {eol}" + f" | | X | | | | | {eol}" + f" | | | | | X | | {eol}" + f" | | | | X | X | | {eol}" + f" | | | | | | | " + ) + assert str(generator) == flip + + generator.tick() + assert str(generator) == flop + + generator.tick() + assert str(generator) == flip diff --git a/project_example/tests/game_of_life/test_string_builder_should.py b/project_example/tests/game_of_life/test_string_builder_should.py new file mode 100644 index 0000000..edcc18a --- /dev/null +++ b/project_example/tests/game_of_life/test_string_builder_should.py @@ -0,0 +1,22 @@ +from game_of_life.string_builder import StringBuilder +from os import linesep as eol + +class TestStringBuilderShould: + + + def test_append_two_strings(self): + string_builder = StringBuilder() + + string_builder.add('expect').add('ed') + + assert string_builder.to_string() == 'expected' + + def test_new_line_two_strings(self): + string_builder = StringBuilder() + + string_builder.add('line 1').newline().add('line 2') + + assert string_builder.to_string() == ( + f"line 1{eol}" + "line 2" + ) \ No newline at end of file diff --git a/project_example/tests/health/__init__.py b/project_example/tests/health/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/tests/health/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/tests/health/conftest.py b/project_example/tests/health/conftest.py new file mode 100644 index 0000000..537f93b --- /dev/null +++ b/project_example/tests/health/conftest.py @@ -0,0 +1,12 @@ +from utilities.date import days_ago +from pytest import fixture +from health.prescription import Prescription + + +@fixture +def prescription(): + """Provides a default prescription""" + return Prescription( + description="Codeine", + dispenseDate=days_ago(days=2), + daysSupply=4) diff --git a/project_example/tests/health/test_prescription_pytest.py b/project_example/tests/health/test_prescription_pytest.py new file mode 100644 index 0000000..e24f9a4 --- /dev/null +++ b/project_example/tests/health/test_prescription_pytest.py @@ -0,0 +1,17 @@ +from datetime import date, timedelta +from utilities.date import days_ago + + +def test_days_taken_and_exclude_future_dates(prescription): + actual = list(prescription.days_taken()) + + assert [days_ago(days=2), days_ago(days=1)] == actual + + +def test_prescription_as_string(prescription): + today = date.today() + expected_date = today - timedelta(days=2) + + actual = prescription.to_string() + assert actual == F"Codeine should be dispensed on the '{expected_date}' with only 4 days supply" + diff --git a/project_example/tests/health/test_prescriptions_unittest.py b/project_example/tests/health/test_prescriptions_unittest.py new file mode 100644 index 0000000..d44a0ba --- /dev/null +++ b/project_example/tests/health/test_prescriptions_unittest.py @@ -0,0 +1,23 @@ +from datetime import date, timedelta +from utilities.date import days_ago +from health.prescription import Prescription +from unittest.case import TestCase + + +class PrescriptionShould(TestCase): + def setUp(self) -> None: + self.prescription = Prescription( + description="Codeine", + dispenseDate=days_ago(days=2), + daysSupply=4) + + def test_days_taken_and_exclude_future_dates(self): + actual = list(self.prescription.days_taken()) + + self.assertListEqual([days_ago(days=2), days_ago(days=1)], actual) + + def test_prescription_as_string(self): + today = date.today() + expected_date = today - timedelta(days=2) + self.assertEqual(self.prescription.to_string( + ), F"Codeine should be dispensed on the '{expected_date}' with only 4 days supply") diff --git a/project_example/tests/sso/__init__.py b/project_example/tests/sso/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/tests/sso/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/tests/sso/conftest.py b/project_example/tests/sso/conftest.py new file mode 100644 index 0000000..a21a91a --- /dev/null +++ b/project_example/tests/sso/conftest.py @@ -0,0 +1,8 @@ +from pytest import fixture +from sso.my_service import MyService + + +@fixture +def my_service(): + "Provides a default Service" + return MyService(None) diff --git a/project_example/tests/sso/test_my_service.py b/project_example/tests/sso/test_my_service.py new file mode 100644 index 0000000..837deb3 --- /dev/null +++ b/project_example/tests/sso/test_my_service.py @@ -0,0 +1,55 @@ +from sso.single_sign_on import SingleSignOnRegistry +from sso.sso_token import SSOToken +from sso.request import Request +from sso.my_service import MyService +from unittest.mock import Mock + + +# def test_hello_name(my_service: MyService): +def test_service_handler_requests_hello_name(): + stub_sso_registry = Mock(SingleSignOnRegistry) + my_service = MyService(stub_sso_registry) + + actual = my_service.handle(Request("Vincent"), SSOToken()) + + assert actual.text == "Hello Vincent" + + +def test_sso_registry_used_to_validate_the_token(): + spy_on_sso_registry = Mock(SingleSignOnRegistry) + my_service = MyService(spy_on_sso_registry) + token = SSOToken() + + my_service.handle(Request("Vincent"), token) + + spy_on_sso_registry.is_valid.assert_called_with(token) + + +def test_invalid_sso_should_get_please_sign_in_response(): + spy_on_sso_registry = Mock(SingleSignOnRegistry) + spy_on_sso_registry.is_valid.return_value = False + my_service = MyService(spy_on_sso_registry) + token = SSOToken() + + actual_response = my_service.handle(Request("Vincent"), token) + + assert actual_response.text == "Please sign in" + + +def test_sso_receives_the_correct_token(): + mock_sso_registry = Mock(SingleSignOnRegistry) + correct_token = SSOToken() + mock_sso_registry.is_valid = Mock(side_effect=confirm_token(correct_token)) + my_service = MyService(mock_sso_registry) + + my_service.handle(Request("Vincent"), correct_token) + + mock_sso_registry.is_valid.assert_called() + + +def confirm_token(correct_token: SSOToken): + def is_valid(actual_token): + if actual_token != correct_token: + raise ValueError("Wrong token received") + + return is_valid diff --git a/project_example/tests/tennis/__init__.py b/project_example/tests/tennis/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/tests/tennis/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/tests/tennis/test_scoring_should_pytest.py b/project_example/tests/tennis/test_scoring_should_pytest.py new file mode 100644 index 0000000..b294240 --- /dev/null +++ b/project_example/tests/tennis/test_scoring_should_pytest.py @@ -0,0 +1,16 @@ + +from tennis.scoring import tennis_score +import pytest + + +@pytest.mark.parametrize("player_one_score, player_two_score, expected_result", + [(0, 0, "Love-All"), + (1, 1, "Fifteen-All"), + (2, 2, "Thirty-All"), + (3, 3, "Forty-All"), + (1, 0, "Fifteen-Love"), + (0, 1, "Love-Fifteen"), + ]) +def test_scoring(player_one_score: int, player_two_score: int, expected_result: str): + actual_score = tennis_score(player_one_score, player_two_score) + assert actual_score == expected_result diff --git a/project_example/tests/tennis/test_scoring_should_unittest.py b/project_example/tests/tennis/test_scoring_should_unittest.py new file mode 100644 index 0000000..e5fe87e --- /dev/null +++ b/project_example/tests/tennis/test_scoring_should_unittest.py @@ -0,0 +1,21 @@ +from tennis.scoring import tennis_score +from unittest import TestCase + + +class TennisScoringShould(TestCase): + + def test_score(self): + test_cases = [ + (0, 0, "Love-All"), + (1, 1, "Fifteen-All"), + (2, 2, "Thirty-All"), + (3, 3, "Forty-All"), + (1, 0, "Fifteen-Love"), + (0, 1, "Love-Fifteen"), + + ] + for player_one_points, player_two_points, expected_result in test_cases: + with self.subTest(f"{player_one_points}{player_two_points} -> {expected_result}"): + self.assertEqual( + tennis_score(player_one_points, player_two_points), + expected_result) diff --git a/project_example/tests/tire_pressure/__init__.py b/project_example/tests/tire_pressure/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/project_example/tests/tire_pressure/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/project_example/tests/tire_pressure/conftest.py b/project_example/tests/tire_pressure/conftest.py new file mode 100644 index 0000000..7309e7a --- /dev/null +++ b/project_example/tests/tire_pressure/conftest.py @@ -0,0 +1,8 @@ +from pytest import fixture +from tire_pressure.alarm import Alarm + + +@fixture +def alarm(): + """Provides a default alarm""" + return Alarm() diff --git a/project_example/tests/tire_pressure/test_alarm_should_pytest.py b/project_example/tests/tire_pressure/test_alarm_should_pytest.py new file mode 100644 index 0000000..b3dc79a --- /dev/null +++ b/project_example/tests/tire_pressure/test_alarm_should_pytest.py @@ -0,0 +1,74 @@ +from tire_pressure.sensor import Sensor +from tire_pressure.alarm import Alarm +from unittest.mock import Mock, patch + + +def test_alarm_is_off_by_default(alarm: Alarm): + assert alarm.is_alarm_on is False + + +def test_low_pressure_activates_alarm(): + alarm = Alarm(sensor=mock_sensor(10)) + + alarm.check() + + assert alarm.is_alarm_on is True + + +def test_high_pressure_activates_alarm(): + alarm = Alarm(sensor=mock_sensor(100)) + + alarm.check() + + assert alarm.is_alarm_on is True + + +def test_call_sensor_sample_pressure(): + sensor_spy = mock_sensor(100) + alarm = Alarm(sensor=sensor_spy) + + alarm.check() + + sensor_spy.sample_pressure.assert_called_once() + + +# REMARKS: Patch makes more sense if the constructor does not allow for sensor to be assigned + + +def test_high_pressure_activates_alarm_using_monkeypatch(): + # REMARKS refers to the modules in the patch param + with patch("tire_pressure.alarm.Sensor") as sensorType: + mock_instance = Mock(Sensor) + mock_instance.sample_pressure.return_value = 22 + sensorType.return_value = mock_instance + + alarm = Alarm() + alarm.check() + + assert alarm.is_alarm_on is True + + +@patch("tire_pressure.alarm.Sensor") +def test_low_pressure_activates_alarm_using_monkeypatch_decorator(sensor_type): + print(sensor_type) + mock_instance = Mock(Sensor) + mock_instance.sample_pressure.return_value = 14 + sensor_type.return_value = mock_instance + + alarm = Alarm() + alarm.check() + + assert alarm.is_alarm_on is True + + +def test_valid_pressure_does_not_activate_alarm(): + alarm = Alarm(sensor=mock_sensor(18)) + alarm.check() + + assert alarm.is_alarm_on is False + + +def mock_sensor(pressure: int): + result = Mock(Sensor) + result.sample_pressure.return_value = pressure + return result diff --git a/project_example/tests/utilities/test_date_pytest.py b/project_example/tests/utilities/test_date_pytest.py new file mode 100644 index 0000000..322e9d6 --- /dev/null +++ b/project_example/tests/utilities/test_date_pytest.py @@ -0,0 +1,15 @@ +from datetime import date +from utilities.date import days_ago + + +# @pytest.skip('actual.day failing for some odd reason') +def test_date_generated_is_correct(): + day_count = 2 + today = date.today() + expected_day = today.day - day_count + + actual = days_ago(days=day_count) + + assert actual.day == expected_day + assert actual.month == today.month + assert actual.year == today.year diff --git a/project_example/tests/utilities/test_date_unittest.py b/project_example/tests/utilities/test_date_unittest.py new file mode 100644 index 0000000..3aec5da --- /dev/null +++ b/project_example/tests/utilities/test_date_unittest.py @@ -0,0 +1,15 @@ +from datetime import date +from unittest.case import TestCase +from utilities.date import days_ago + + +class DaysAgoShould(TestCase): + def test_date_generated_is_correct(self): + day_count = 2 + today = date.today() + + actual = days_ago(day_count) + + self.assertEqual(actual.day, today.day - day_count) + self.assertEqual(actual.month, today.month) + self.assertEqual(actual.year, today.year) diff --git a/project_example/tire_pressure/__init__.py b/project_example/tire_pressure/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/tire_pressure/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/tire_pressure/alarm.py b/project_example/tire_pressure/alarm.py new file mode 100644 index 0000000..599af7f --- /dev/null +++ b/project_example/tire_pressure/alarm.py @@ -0,0 +1,18 @@ +from tire_pressure.sensor import Sensor + + +class Alarm: + def __init__(self, sensor=None) -> None: + self._lowPressureThreshold = 17 + self._highPressureThreshold = 21 + self._sensor = sensor or Sensor() + self._isAlarmOn = False + + @property + def is_alarm_on(self): + return self._isAlarmOn + + def check(self): + pressure = self._sensor.sample_pressure() + if pressure < self._lowPressureThreshold or pressure > self._highPressureThreshold: + self._isAlarmOn = True diff --git a/project_example/tire_pressure/sensor.py b/project_example/tire_pressure/sensor.py new file mode 100644 index 0000000..0d84712 --- /dev/null +++ b/project_example/tire_pressure/sensor.py @@ -0,0 +1,6 @@ +class Sensor: + def __init__(self, pressure = 18): + self._pressure = pressure + + def sample_pressure(self): + return self._pressure; diff --git a/project_example/utilities/__init__.py b/project_example/utilities/__init__.py new file mode 100644 index 0000000..991aa1a --- /dev/null +++ b/project_example/utilities/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project_example/utilities/date.py b/project_example/utilities/date.py new file mode 100644 index 0000000..3aa6b81 --- /dev/null +++ b/project_example/utilities/date.py @@ -0,0 +1,4 @@ +from datetime import date, timedelta + +def days_ago(days: int) -> date: + return date.today() - timedelta(days=days) diff --git a/vscode-test-explorer.png b/vscode-test-explorer.png new file mode 100644 index 0000000..e1b9f13 Binary files /dev/null and b/vscode-test-explorer.png differ