Skip to content

Commit 33134ef

Browse files
committed
Merge branch 'release/v2.0.2'
2 parents bcb4ac4 + f29c21e commit 33134ef

13 files changed

Lines changed: 360 additions & 242 deletions

.github/workflows/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ This guide helps developers set up, test, and deploy the Flex Net Sim Backend AP
5555
Run the test suite to ensure code quality:
5656

5757
```bash
58-
pytest --cov=backend tests/
58+
pytest --cov=backend --cov=utils tests/
5959
```
6060

6161
## Docker Deployment

.github/workflows/fns-api-workflow.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
4646
- name: Run tests with coverage
4747
run: |
48-
pytest --cov=backend --cov-report=html tests/
48+
pytest --cov=backend --cov=utils --cov-report=html tests/
4949
5050
- name: Prepare coverage report for GitHub Pages
5151
run: |

CHANGELOG.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### ROADMAP
99
- Switch to new domain.
1010

11+
## [2.0.2] - 2025-03-12
12+
13+
### Added
14+
- Comprehensive unit tests for parameter validation in simulation requests
15+
- Better validation error messages for all simulation parameters
16+
- Helper methods for test assertions to improve code readability
17+
- Added fallback mechanism to recompile the simulation executable if prerequisites are not met in both `/run_simulation` and `/run_simulation_stream` endpoints.
18+
19+
### Changed
20+
- Enhanced test coverage for the helpers module
21+
- Improved test structure with dedicated test methods per parameter
22+
23+
### Fixed
24+
- Edge case handling in parameter validation
25+
26+
1127
## [2.0.1] - 2025-03-04
1228

1329
### Fixed
@@ -88,7 +104,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88104
- Documentation README for the process of develop/deployment located in [workflows](https://github.com/MirkoZETA/FlexNetSim-API/tree/master/.github/workflows/README_DEV.md).
89105

90106
[1.0.0]: https://github.com/MirkoZETA/FlexNetSim-API/releases/tag/v1.0.0
91-
[1.1.0]: https://github.com/MirkoZETA/FlexNetSim-API/releases/tag/v1.1.0
92-
[1.1.1]: https://github.com/MirkoZETA/FlexNetSim-API/releases/tag/pipeline-fix
93-
[2.0.0]: https://github.com/MirkoZETA/FlexNetSim-API/releases/tag/v2.0.0
94-
[2.0.1]: https://github.com/MirkoZETA/FlexNetSim-API/releases/tag/v2.0.1
107+
[1.1.0]: https://github.com/MirkoZETA/FlexNetSim-API/compare/v1.0.0...v1.1.0
108+
[1.1.1]: https://github.com/MirkoZETA/FlexNetSim-API/compare/v1.1.0...pipeline-fix
109+
[2.0.0]: https://github.com/MirkoZETA/FlexNetSim-API/compare/pipeline-fix...v2.0.0
110+
[2.0.1]: https://github.com/MirkoZETA/FlexNetSim-API/compare/v2.0.0...v2.0.1
111+
[2.0.2]: https://github.com/MirkoZETA/FlexNetSim-API/compare/v2.0.1...v2.0.2

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
![Static Badge](https://img.shields.io/badge/language-python-blue)
55
[![Static Badge](https://img.shields.io/badge/licese-MIT-green)](https://github.com/MirkoZETA/FlexNetSim-API/blob/master/LICENSE)
66
[![Static Badge](https://github.com/MirkoZETA/FlexNetSim-API/actions/workflows/fns-api-workflow.yml/badge.svg)](https://github.com/MirkoZETA/FlexNetSim-API/actions/workflows/fns-api-workflow.yml)
7-
[![Static Badge](https://img.shields.io/badge/coverage-95%25-brightgreen)](https://mirkozeta.github.io/FlexNetSim-API/coverage/)
7+
[![Static Badge](https://img.shields.io/badge/coverage-93%25-brightgreen)](https://mirkozeta.github.io/FlexNetSim-API/coverage/)
88

99
A lightweight API for running optical network simulations with [Flex Net Sim C++](https://gitlab.com/DaniloBorquez/flex-net-sim).
1010

backend.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def run_simulation():
2727
# Validate prerequisites
2828
is_valid, error_response = validate_simulation_prerequisites()
2929
if not is_valid:
30+
compile_simulation(True)
3031
return error_response
3132

3233
try:
@@ -88,6 +89,7 @@ def run_simulation_stream():
8889
# Validate prerequisites
8990
is_valid, error_response = validate_simulation_prerequisites()
9091
if not is_valid:
92+
compile_simulation(True)
9193
return error_response
9294

9395
try:

src/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Modification Notice: simulation.hpp
1+
## Modification Notice: `simulation.hpp`
22

33
This document describes a modification made to the `simulation.hpp` file, specifically in the `Simulator::printRow` function. The purpose of this change is to enable real-time streaming of simulation output when used with the API, ensuring that data is delivered to the client as soon as it is available.
44

@@ -29,4 +29,4 @@ void Simulator::printRow(double percentage) {
2929

3030
### Future Considerations
3131

32-
If future versions of the library include this feature, this modification may no longer be necessary. Until then, it should be retained to ensure proper streaming behavior.
32+
If future versions of the library include this feature, this modification may no longer be necessary. Until then, it should be retained to ensure proper streaming behavior.

tests/test_compilation.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from flask_testing import TestCase
44
from backend import app, compile_simulation
5+
from tests.test_utils import temporarily_rename_file
56
import json
67
import os
78

9+
810
class TestCompilation(TestCase):
911
"""Tests for simulation compilation"""
1012

@@ -13,27 +15,27 @@ def create_app(self):
1315
return app
1416

1517
def test_compilation_failure(self):
16-
os.rename("./src/main.cpp", "./src/main.cpp.temp")
17-
compile_result = compile_simulation(debug=True)
18-
os.rename("./src/main.cpp.temp", "./src/main.cpp")
19-
self.assertFalse(compile_result)
18+
# Test with missing main.cpp file
19+
with temporarily_rename_file("./src/main.cpp", "./src/main.cpp.temp"):
20+
compile_result = compile_simulation(debug=True)
21+
self.assertFalse(compile_result)
2022

21-
response = self.client.post('/run_simulation',
22-
data=json.dumps({}),
23-
content_type='application/json')
24-
self.assert_status(response, 500)
23+
response = self.client.post('/run_simulation',
24+
data=json.dumps({}),
25+
content_type='application/json')
26+
self.assert_status(response, 500)
2527

26-
os.rename("./src/main.cpp", "./src/main.cpp.temp")
27-
os.rename("./src/test_main.cpp", "./src/main.cpp")
28-
compile_result = compile_simulation(debug=True)
29-
self.assertFalse(compile_result)
30-
31-
response = self.client.post('/run_simulation',
32-
data=json.dumps({}),
33-
content_type='application/json')
34-
self.assert_status(response, 500)
35-
os.rename("./src/main.cpp", "./src/test_main.cpp")
36-
os.rename("./src/main.cpp.temp", "./src/main.cpp")
28+
# Test with invalid main.cpp file
29+
if os.path.exists("./src/test_main.cpp"):
30+
with temporarily_rename_file("./src/main.cpp", "./src/main.cpp.temp"):
31+
with temporarily_rename_file("./src/test_main.cpp", "./src/main.cpp"):
32+
compile_result = compile_simulation(debug=True)
33+
self.assertFalse(compile_result)
34+
35+
response = self.client.post('/run_simulation',
36+
data=json.dumps({}),
37+
content_type='application/json')
38+
self.assert_status(response, 500)
3739

3840
def test_compilation_success(self):
3941
compile_result = compile_simulation(debug=True)

tests/test_help_endpoint.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from flask_testing import TestCase
44
from backend import app
5+
import pytest
56

67
class TestHelpEndpoint(TestCase):
78
"""Tests for help endpoint"""

tests/test_helpers.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Tests for utils/helpers.py
2+
3+
import pytest
4+
import os
5+
from flask_testing import TestCase
6+
from backend import app
7+
from utils.helpers import *
8+
from tests.test_utils import temporarily_rename_file
9+
10+
class TestHelpers(TestCase):
11+
"""Tests for utils functions"""
12+
13+
def create_app(self):
14+
app.config['TESTING'] = True
15+
return app
16+
17+
def test_compile_simulation(self):
18+
# Test compilation failure
19+
with temporarily_rename_file("./src/main.cpp", "./src/main.cpp.temp"):
20+
self.assertFalse(compile_simulation(debug=True))
21+
22+
# Test compilation success
23+
self.assertTrue(compile_simulation(debug=True))
24+
25+
def test_validate_simulation_prerequisites(self):
26+
# Test when executable is not found
27+
with temporarily_rename_file("./src/simulation.out", "./src/simulation.out.temp_error"):
28+
is_valid, response = validate_simulation_prerequisites()
29+
self.assertFalse(is_valid)
30+
self.assertEqual(response[1], 500)
31+
32+
# Test when executable is found
33+
self.assertTrue(compile_simulation(debug=True))
34+
is_valid, response = validate_simulation_prerequisites()
35+
self.assertTrue(is_valid)
36+
37+
def test_invalid_lambda_param(self):
38+
# Test lambda = 0
39+
is_valid, response = parse_simulation_parameters({"lambdaParam": 0})
40+
self._assert_invalid_param(is_valid, response, "lambdaParam must be greater than 0")
41+
42+
# Test lambda as string
43+
is_valid, response = parse_simulation_parameters({"lambdaParam": "string"})
44+
self._assert_invalid_param(is_valid, response, "lambdaParam must be a number")
45+
46+
def test_invalid_mu_param(self):
47+
# Test mu = 0
48+
is_valid, response = parse_simulation_parameters({"mu": 0})
49+
self._assert_invalid_param(is_valid, response, "mu must be greater than 0")
50+
51+
# Test mu as string
52+
is_valid, response = parse_simulation_parameters({"mu": "string"})
53+
self._assert_invalid_param(is_valid, response, "mu must be a number")
54+
55+
def test_invalid_goal_connections(self):
56+
# Test goalConnections = 0
57+
is_valid, response = parse_simulation_parameters({"goalConnections": 0})
58+
self._assert_invalid_param(is_valid, response, "goalConnections must be greater than 0")
59+
60+
# Test goalConnections too high
61+
is_valid, response = parse_simulation_parameters({"goalConnections": 10000001})
62+
self._assert_invalid_param(is_valid, response, "goalConnections must be less than 10,000,000")
63+
64+
# Test goalConnections as string
65+
is_valid, response = parse_simulation_parameters({"goalConnections": "string"})
66+
self._assert_invalid_param(is_valid, response, "goalConnections must be an integer")
67+
68+
def test_invalid_network_type(self):
69+
# Test unsupported network type
70+
is_valid, response = parse_simulation_parameters({"networkType": 99})
71+
self._assert_invalid_param(is_valid, response, "At the moment only networkType 1 is supported")
72+
73+
# Test network type as string
74+
is_valid, response = parse_simulation_parameters({"networkType": "string"})
75+
self._assert_invalid_param(is_valid, response, "networkType must be an integer")
76+
77+
def test_invalid_confidence(self):
78+
# Test confidence too high
79+
is_valid, response = parse_simulation_parameters({"confidence": 1.1})
80+
self._assert_invalid_param(is_valid, response, "confidence must be between 0 and 1")
81+
82+
# Test confidence too low
83+
is_valid, response = parse_simulation_parameters({"confidence": -0.1})
84+
self._assert_invalid_param(is_valid, response, "confidence must be between 0 and 1")
85+
86+
# Test confidence as string
87+
is_valid, response = parse_simulation_parameters({"confidence": "string"})
88+
self._assert_invalid_param(is_valid, response, "confidence must be a number")
89+
90+
def test_invalid_algorithm(self):
91+
# Test invalid algorithm name
92+
is_valid, response = parse_simulation_parameters({"algorithm": "InvalidAlgorithm"})
93+
self._assert_invalid_param(is_valid, response, "algorithm must be FirstFit or BestFit")
94+
95+
# Test algorithm as number
96+
is_valid, response = parse_simulation_parameters({"algorithm": 123})
97+
self._assert_invalid_param(is_valid, response, "algorithm must be a string")
98+
99+
def test_invalid_k(self):
100+
# Test K too small
101+
is_valid, response = parse_simulation_parameters({"K": 0})
102+
self._assert_invalid_param(is_valid, response, "Min K is 1")
103+
104+
# Test K too large
105+
is_valid, response = parse_simulation_parameters({"K": 100})
106+
self._assert_invalid_param(is_valid, response, "Max K is 6")
107+
108+
# Test K as string
109+
is_valid, response = parse_simulation_parameters({"K": "string"})
110+
self._assert_invalid_param(is_valid, response, "K must be an integer")
111+
112+
def test_invalid_network(self):
113+
# Test invalid network name
114+
is_valid, response = parse_simulation_parameters({"network": "TestNetwork"})
115+
self._assert_invalid_param(is_valid, response, "network must be one of")
116+
117+
# Test network as number
118+
is_valid, response = parse_simulation_parameters({"network": 123})
119+
self._assert_invalid_param(is_valid, response, "network must be a string")
120+
121+
def test_invalid_bitrate(self):
122+
# Test invalid bitrate name
123+
is_valid, response = parse_simulation_parameters({"bitrate": "TestBitRate"})
124+
self._assert_invalid_param(is_valid, response, "bitrate must be one of")
125+
126+
# Test bitrate as number
127+
is_valid, response = parse_simulation_parameters({"bitrate": 123})
128+
self._assert_invalid_param(is_valid, response, "bitrate must be a string")
129+
130+
def _assert_invalid_param(self, is_valid, response, expected_error):
131+
"""Helper method to validate common assertions for invalid parameters"""
132+
self.assertFalse(is_valid)
133+
response_json = response[0].json
134+
self.assertEqual(response_json["status"], "error")
135+
self.assertEqual(response_json["message"], "Invalid parameters")
136+
self.assertIn(expected_error, response_json["error"])
137+
138+
def test_parse_simulation_parameters_valid(self):
139+
data = {
140+
"algorithm": "FirstFit",
141+
"networkType": 1,
142+
"goalConnections": 10,
143+
"confidence": 0.05,
144+
"lambdaParam": 1,
145+
"mu": 10,
146+
"network": "NSFNet",
147+
"bitrate": "fixed-rate",
148+
"K": 3
149+
}
150+
is_valid, params = parse_simulation_parameters(data)
151+
self.assertTrue(is_valid)
152+
self.assertEqual(params, ("FirstFit", 1, 10, 0.05, 1, 10, "NSFNet", "fixed-rate", 3))
153+
154+
def test_build_simulation_command(self):
155+
params = ("FirstFit", 1, 10, 0.05, 1, 10, "NSFNet", "fixed-rate", 3)
156+
command = build_simulation_command(params)
157+
expected_command = [
158+
f"./{SIMULATION_EXECUTABLE}",
159+
"FirstFit",
160+
"1",
161+
"10",
162+
"0.05",
163+
"1",
164+
"10",
165+
"NSFNet",
166+
"fixed-rate",
167+
"3"
168+
]
169+
self.assertEqual(command, expected_command)

0 commit comments

Comments
 (0)