Skip to content

Commit a5c7167

Browse files
authored
Merge pull request #28 from Pallandos/add-test
Add unit test for Python 3.11 and 3.12
2 parents be86797 + 6f3b93e commit a5c7167

25 files changed

+1705
-58
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This is the configuration file
22

33
# ==== network ====
4-
NODE_NUMBER=5
4+
NODE_NUMBER=8
55
MAX_PEERS=128
66
# WARNING : set this value high (don't know why, but if it's
77
# under ~11 , a node will refuse all connections)

.github/workflows/python-tests.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Python application
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
9+
strategy:
10+
matrix:
11+
python-version: ['3.11', '3.12']
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: ${{ matrix.python-version }}
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install pytest pytest-cov codecov
25+
pip install -r requirements.txt
26+
27+
- name: Run tests with coverage
28+
run: |
29+
pytest --cov=py --cov-report=xml:tests/coverage.xml --cov-report=term tests/
30+
31+
- name: Upload coverage to Codecov
32+
uses: codecov/codecov-action@v4
33+
with:
34+
files: ./tests/coverage.xml

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ img/
55
__pycache__/*
66
*.pyc
77

8-
/py/tests.py
8+
# tests
9+
.coverage
10+
11+
# IDE
12+
.vscode/

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ Usage: ./bitcoin-on-local.sh start|stop|renew|draw|scenario|draw [output_file]
7474
This tool requires :
7575

7676
- Docker (compose v2)
77-
- Python (>= 3.12)[^1]
77+
- Python (>= 3.11)[^1]
7878

79-
[^1]: The tool may work on older versions but has not been tested on it.
79+
[^1]: The tool has been tested on Python 3.11 and 3.12
8080

8181
> [!NOTE]
8282
> Project is beeing tested right now

py/generate_compose.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# this file generates a docker-compose.yml file based on the provided configuration
22

33
import random
4+
import os
45

56
from config import (
67
NODE_NUMBER,
@@ -130,6 +131,9 @@ def export_data(all_ports: dict, node_names: list, output_dir: str = 'data'):
130131
output_dir (str): Subdirectory of /docker to store the .env files. Defaults to 'data'.
131132
"""
132133

134+
# ensure the output directory exists
135+
os.makedirs(f"docker/{output_dir}", exist_ok=True)
136+
133137
# export node names :
134138
output_file_names = f"docker/{output_dir}/.env.node_names"
135139
with open(output_file_names, 'w') as file:

py/network_info/get_info.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,4 @@ def get_peer_info(node_name : str):
2020
return json.loads(result.stdout)
2121
except Exception as e:
2222
print(f"Error for {node_name}: {e}")
23-
return []
24-
25-
if __name__ == "__main__":
26-
# tests
27-
node_1 = "node_1"
28-
info = get_peer_info(node_1)
29-
print(info)
23+
return []

py/network_info/parse.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,4 @@ def extract_connections(peers_info):
6666
peer_name = peer_addr
6767
connections.append((peer_name,connection_type))
6868

69-
return connections
70-
71-
if __name__ == "__main__":
72-
#tests
73-
74-
name = _docker_dns("172.20.0.7")
75-
print(name)
69+
return connections

py/scenario/rpc_caller.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ def call(self, node: str, method: str, params: list = None) -> Any:
6060

6161
return result.get('result', None)
6262

63+
except BitcoinRPCError:
64+
# Re-raise the BitcoinRPCError to avoid the final except block
65+
raise
6366
except requests.ConnectionError as e:
6467
raise requests.ConnectionError(f"Connection failed to {node}") from e
6568
except requests.Timeout as e:

py/scenario/runner.py

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,66 @@
22
from .loader import ScenarioLoader
33
from .rpc_caller import BitcoinRPC
44
from .actions import ActionExecutor
5-
from typing import Dict, Any, List
5+
from typing import Dict, Any
6+
67

78
# Error classes for ScenarioRunner
89
class ScenarioRunnerError(Exception):
910
"""Base class for all scenario runner errors."""
11+
1012
pass
13+
14+
1115
class ScenarioNotLoadedError(ScenarioRunnerError):
1216
"""Raised when a scenario is tried to be runned but not loaded."""
17+
1318
def __init__(self):
1419
super().__init__("No scenario loaded. Please load a scenario first.")
1520

21+
1622
class ScenarioRunner:
17-
"""ScenarioRunner is a class that manages the execution of Bitcoin scenarios.
18-
"""
19-
def __init__(self,
20-
rpc_user : str,
21-
rpc_password : str,
22-
scenarios_dir : str = "./scenarios",
23-
base_port : int = 18443,
24-
):
25-
23+
"""ScenarioRunner is a class that manages the execution of Bitcoin scenarios."""
24+
25+
def __init__(
26+
self,
27+
rpc_user: str,
28+
rpc_password: str,
29+
scenarios_dir: str = "./scenarios",
30+
base_port: int = 18443,
31+
):
2632
self.loader = ScenarioLoader(scenarios_dir)
27-
self.variables = {} # Store scenario variables
28-
33+
self.variables = {} # Store scenario variables
34+
2935
rpc = BitcoinRPC(rpc_user, rpc_password, base_port)
3036
self.executor = ActionExecutor(rpc)
31-
37+
3238
self.scenario = None # Will hold the loaded scenario
33-
self.config = None # Will hold the scenario configuration
34-
35-
def load_scenario(self, scenario_name : str):
39+
self.config = None # Will hold the scenario configuration
40+
41+
def load_scenario(self, scenario_name: str):
3642
"""Load a scenario by its name. Required before running it.
3743
3844
Args:
3945
scenario_name (str): Name of the scenario to load (without .toml extension).
4046
"""
41-
47+
4248
self.scenario = self.loader.load_scenario(scenario_name)
43-
self.config = self.scenario["config"] # so we dont have to load it again
44-
49+
self.config = self.scenario["config"] # so we dont have to load it again
50+
4551
# print infos :
4652
print("==========================================")
4753
print(f"Name: {self.scenario['scenario']['name']} \n")
4854
print(f"Description: {self.scenario['scenario']['description']}\n")
4955
print(f"Written by: {self.scenario['scenario']['author']}\n")
5056
print(f"Date: {self.scenario['scenario']['date']}")
5157
print("==========================================")
52-
58+
5359
def list_scenarios(self):
5460
"""List available scenarios"""
5561
print("Available scenarios:")
5662
scenarios = self.loader.list_scenarios()
5763
print("\n".join(scenarios) if scenarios else "No scenarios found.")
58-
64+
5965
def _substitute_variables(self, params: Any) -> Any:
6066
"""Replace ${var} with actual values"""
6167
if isinstance(params, str):
@@ -67,52 +73,53 @@ def _substitute_variables(self, params: Any) -> Any:
6773
elif isinstance(params, list):
6874
return [self._substitute_variables(item) for item in params]
6975
return params
70-
76+
7177
# ==== runners ====
72-
73-
def _run_step(self, step: Dict[str , Any]) -> None:
74-
78+
79+
def _run_step(self, step: Dict[str, Any]) -> None:
7580
# exctract actions details
7681
# → the scenario is valid so we can assume that the step has the required keys
7782
action_name = step["name"]
7883
action = step["action"]
7984
node = step.get("node", self.config["default_node"])
8085
args = self._substitute_variables(step.get("args", {}))
81-
86+
8287
# execute the action
8388
print(f"Running step: {action_name} (on node: {node})")
8489
result = self.executor.execute(action, node, args)
85-
90+
8691
# == deal with options ==
8792
if step.get("print", False):
8893
print(f"Result: \n{result} \n")
89-
if args.get("store_result",""):
94+
if args.get("store_result", ""):
9095
# store the result in the variables dict
9196
var_name = args.get("store_result")
9297
self.variables[var_name] = result
9398
print(f"Stored result in variable: {var_name} = {result}")
94-
99+
95100
# time and wait
96101
time.sleep(step.get("wait_after", self.config["default_wait"]))
97-
102+
98103
def run_scenario(self) -> None:
99104
"""Run the loaded scenario step by step"""
100-
if not hasattr(self, 'scenario'):
105+
if self.scenario is None:
101106
raise ScenarioNotLoadedError()
102-
107+
103108
print(f"[SCENARIO] Running scenario: {self.scenario['scenario']['name']}")
104-
109+
105110
# timeout sleep:
106111
time.sleep(self.config.get("timeout"))
107-
112+
108113
for i, ststep_name in enumerate(self.scenario["steps"]):
109114
print(f"\n[SCENARIO] Step {i + 1}/{len(self.scenario['steps'])}")
110115
step = self.scenario["steps"][ststep_name]
111-
116+
112117
try:
113118
self._run_step(step)
114119
except Exception as e:
115-
print(f"[ERROR] An error occurred while running step '{ststep_name}': {e}")
120+
print(
121+
f"[ERROR] An error occurred while running step '{ststep_name}': {e}"
122+
)
116123
raise e
117-
118-
print("[SCENARIO] Scenario execution completed.")
124+
125+
print("[SCENARIO] Scenario execution completed.")

pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
testpaths = tests
3+
python_files = test_*.py
4+
pythonpath = py

0 commit comments

Comments
 (0)