diff --git a/examples/algorithms/__init__.py b/examples/algorithms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/config/pytest.toml b/examples/config/pytest.toml new file mode 100644 index 0000000..a92d8cd --- /dev/null +++ b/examples/config/pytest.toml @@ -0,0 +1,10 @@ +simulation_id = "Pytest Simulation" +algorithms_directory = "examples/algorithms/" +nodes = [ { node_id = "node_1", algorithm = { file = "simple.py", object = "AlwaysCooperate" } }, + { node_id = "node_2", algorithm = { file = "simple.py", object = "AlwaysDefect" } }, + { node_id = "node_3", algorithm = { file = "tit_for_tat.py", object = "TitForTat" } }, ] +rounds_data = "examples/rounds/pytest.json" +rounds_output = "examples/rounds/pytest.json" +simulation = { object = "StandardSimulation" } +simulation_arguments = { rounds = 10 } +simulation_output = "examples/results/pytest.json" \ No newline at end of file diff --git a/examples/results/pytest.json b/examples/results/pytest.json new file mode 100644 index 0000000..ece3e30 --- /dev/null +++ b/examples/results/pytest.json @@ -0,0 +1 @@ +{"node_1": 30, "node_2": 100, "node_3": 30} \ No newline at end of file diff --git a/examples/rounds/pytest.json b/examples/rounds/pytest.json new file mode 100644 index 0000000..41798f4 --- /dev/null +++ b/examples/rounds/pytest.json @@ -0,0 +1 @@ +{"node_1:node_2": [{"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}, {"node_1": true, "node_2": false}], "node_1:node_3": [{"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}, {"node_1": true, "node_3": true}], "node_2:node_3": [{"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}, {"node_2": false, "node_3": true}]} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index cf7636f..6c57622 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,12 @@ classifiers = [ "Topic :: Games/Entertainment :: Simulation" ] dependencies = [ - "platformdirs~=4.1.0" + "platformdirs" +] + +[project.optional-dependencies] +test = [ + "pytest" ] [project.urls] @@ -41,4 +46,14 @@ Repository = "https://github.com/Macr0Nerd/project_dilemma" project_dilemma = "project_dilemma.__main__:main" [tool.setuptools.dynamic] -version = {attr = "project_dilemma.__version__"} \ No newline at end of file +version = {attr = "project_dilemma.__version__"} + +[tool.pytest.ini_options] +addopts = "-ra" +pythonpath = [ + "src", + "examples" +] +testpaths = [ + "tests" +] \ No newline at end of file diff --git a/src/project_dilemma/config.py b/src/project_dilemma/config.py index b5e5f88..793df5c 100644 --- a/src/project_dilemma/config.py +++ b/src/project_dilemma/config.py @@ -60,7 +60,7 @@ def arguments() -> dict: parser_out.add_argument('-sO', '--simulation-output', help='output the results as JSON', dest='simulation_output') - if not len(sys.argv): + if len(sys.argv) <= 1: parser.print_help() sys.exit(0) diff --git a/src/project_dilemma/interfaces/node.py b/src/project_dilemma/interfaces/node.py index 8ad0764..4903bbb 100644 --- a/src/project_dilemma/interfaces/node.py +++ b/src/project_dilemma/interfaces/node.py @@ -14,8 +14,9 @@ limitations under the License. """ import random +from typing import Self, Type -from project_dilemma.interfaces.algorithm import Algorithm +from project_dilemma.interfaces import Algorithm from project_dilemma.interfaces.base import Base @@ -27,7 +28,7 @@ class Node(Base): :var node_id: id of the node :vartype node_id: str :var algorithm: cooperation algorithm - :vartype algorithm: Algorithm + :vartype algorithm: Type[Algorithm] """ _required_attributes = [ 'algorithm', @@ -36,12 +37,15 @@ class Node(Base): ] node_id: str - algorithm: Algorithm + algorithm: Type[Algorithm] - def __init__(self, node_id: str, algorithm: Algorithm): + def __init__(self, node_id: str, algorithm: Type[Algorithm]): self.node_id = node_id self.algorithm = algorithm + def __eq__(self, other: Self): + return (self.node_id == other.node_id) and (self.algorithm.algorithm_id == other.algorithm.algorithm_id) + def mutate(self): """set the node to a random algorithm mutation""" if self.algorithm.mutations: diff --git a/src/project_dilemma/object_loaders.py b/src/project_dilemma/object_loaders.py index e002f04..2ecefa5 100644 --- a/src/project_dilemma/object_loaders.py +++ b/src/project_dilemma/object_loaders.py @@ -1,27 +1,43 @@ import importlib -import _json import json import os.path import sys -from typing import Dict, List +from typing import Dict, List, Type from project_dilemma.config import ProjectDilemmaConfig from project_dilemma.interfaces import Algorithm, Node, Simulation, SimulationRounds from project_dilemma.simulations import simulations_map -def load_algorithms(config: ProjectDilemmaConfig) -> Dict[str, Algorithm]: +def create_nodes(config: ProjectDilemmaConfig, algorithms_map: Dict[str, Type[Algorithm]]) -> List[Node]: + """create the simulation nodes + + :param config: configuration data + :type config: ProjectDilemmaConfig + :param algorithms_map: map of algorithm class names to algorithms + :type algorithms_map: Dict[str, Type[Algorithm]] + :return: + """ + nodes = [] + + for node in config['nodes']: + nodes.append(Node(node['node_id'], algorithms_map[node['algorithm']['object']])) + + return nodes + + +def load_algorithms(config: ProjectDilemmaConfig) -> Dict[str, Type[Algorithm]]: """load all algorithms used :param config: configuration data :type config: ProjectDilemmaConfig :return: map of algorithm class names to algorithms - :rtype: Dict[str, Algorithm] + :rtype: Dict[str, Type[Algorithm]] """ sys.path.append(config['algorithms_directory']) algorithms = [node['algorithm'] for node in config['nodes']] - algorithm_map: Dict[str, Algorithm] = {} + algorithm_map: Dict[str, Type[Algorithm]] = {} for algorithm in algorithms: if algorithm_map.get(algorithm['object']): @@ -51,7 +67,7 @@ def load_rounds(config: ProjectDilemmaConfig) -> SimulationRounds: return round_data -def load_simulation(config: ProjectDilemmaConfig) -> Simulation: +def load_simulation(config: ProjectDilemmaConfig) -> Type[Simulation]: """load the simulation :param config: configuration data @@ -76,20 +92,3 @@ def load_simulation(config: ProjectDilemmaConfig) -> Simulation: simulation = simulations_map[config['simulation']['object']] return simulation - - -def create_nodes(config: ProjectDilemmaConfig, algorithms_map: Dict[str, Algorithm]) -> List[Node]: - """create the simulation nodes - - :param config: configuration data - :type config: ProjectDilemmaConfig - :param algorithms_map: map of algorithm class names to algorithms - :type algorithms_map: Dict[str, Algorithm] - :return: - """ - nodes = [] - - for node in config['nodes']: - nodes.append(Node(node['node_id'], algorithms_map[node['algorithm']['object']])) - - return nodes diff --git a/tests/integration_test.py b/tests/integration_test.py new file mode 100644 index 0000000..186eb66 --- /dev/null +++ b/tests/integration_test.py @@ -0,0 +1,111 @@ +import json + +import pytest + +import project_dilemma.config +from project_dilemma.config import load_configuration, ProjectDilemmaConfig +from project_dilemma.interfaces import Node +from project_dilemma.object_loaders import create_nodes, load_algorithms, load_rounds, load_simulation +from project_dilemma.simulations import StandardSimulation + +from algorithms.simple import AlwaysCooperate, AlwaysDefect +from algorithms.tit_for_tat import TitForTat + + +@pytest.fixture +def test_configuration_loading(monkeypatch): + def mock_args(): + return {'config': 'examples/config/pytest.toml'} + + monkeypatch.setattr(project_dilemma.config, 'arguments', mock_args) + + expected: ProjectDilemmaConfig = { + 'simulation_id': 'Pytest Simulation', + 'algorithms_directory': 'examples/algorithms/', + 'nodes': [ + {'node_id': 'node_1', 'algorithm': {'file': 'simple.py', 'object': 'AlwaysCooperate'}}, + {'node_id': 'node_2', 'algorithm': {'file': 'simple.py', 'object': 'AlwaysDefect'}}, + {'node_id': 'node_3', 'algorithm': {'file': 'tit_for_tat.py', 'object': 'TitForTat'}}, + ], + 'rounds_data': 'examples/rounds/pytest.json', + 'rounds_output': 'examples/rounds/pytest.json', + 'simulation': {'object': 'StandardSimulation'}, + 'simulation_arguments': {'rounds': 10}, + 'simulation_output': 'examples/results/pytest.json' + } + + actual = load_configuration() + + assert expected == actual + + return actual + + +@pytest.fixture +def test_object_loading(test_configuration_loading): + with open('examples/rounds/pytest.json', 'r') as f: + expected_rounds = json.load(f) + + actual_rounds = load_rounds(test_configuration_loading) + + assert expected_rounds == actual_rounds + + expected_algorithm_map = { + 'AlwaysCooperate': AlwaysCooperate, + 'AlwaysDefect': AlwaysDefect, + 'TitForTat': TitForTat + } + + actual_algorithm_map = load_algorithms(test_configuration_loading) + + assert False not in [ + algo.algorithm_id == actual_algorithm_map[aid].algorithm_id for aid, algo in expected_algorithm_map.items() + ] + + expected_simulation = StandardSimulation + + actual_simulation = load_simulation(test_configuration_loading) + + assert expected_simulation == actual_simulation + + expected_nodes = [ + Node('node_1', AlwaysCooperate), + Node('node_2', AlwaysDefect), + Node('node_3', TitForTat) + ] + + actual_nodes = create_nodes(test_configuration_loading, expected_algorithm_map) + + assert expected_nodes == actual_nodes + + return actual_simulation( + simulation_id=test_configuration_loading['simulation_id'], + nodes=actual_nodes, + **test_configuration_loading['simulation_arguments'] + ) + + +@pytest.fixture +def test_simulation_run(test_object_loading): + with open('examples/rounds/pytest.json', 'r') as f: + expected_rounds = json.load(f) + + actual_rounds = test_object_loading.run_simulation() + + assert expected_rounds == actual_rounds + + +def test_simulation_process(test_object_loading): + with open('examples/rounds/pytest.json', 'r') as f: + rounds = json.load(f) + + with open('examples/results/pytest.json', 'r') as f: + expected_results = json.load(f) + + test_object_loading.simulation_rounds = rounds + + actual_results = test_object_loading.process_results() + + assert expected_results == actual_results + +