Skip to content

Commit 0d6aef7

Browse files
authored
Merge pull request #32 from TaskarCenterAtUW/stage
Stage to Prod
2 parents 136dfef + dd0673b commit 0d6aef7

18 files changed

+1527
-4
lines changed

.github/workflows/unit_tests.yaml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Unit Tests
2+
on:
3+
workflow_dispatch:
4+
push:
5+
branches-ignore:
6+
- '**'
7+
pull_request:
8+
branches: [ main, dev, stage ]
9+
10+
jobs:
11+
UnitTest:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v2
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v2
20+
with:
21+
python-version: '3.10'
22+
23+
- name: Install dependencies
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install -r requirements.txt
27+
28+
- name: Determine output folder
29+
id: set_output_folder
30+
run: |
31+
if [[ $GITHUB_EVENT_NAME == "pull_request" ]]; then
32+
branch_name=$GITHUB_BASE_REF
33+
else
34+
branch_name=$GITHUB_REF_NAME
35+
fi
36+
37+
if [[ $branch_name == "main" ]]; then
38+
echo "output_folder=prod" >> $GITHUB_ENV
39+
elif [[ $branch_name == "stage" ]]; then
40+
echo "output_folder=stage" >> $GITHUB_ENV
41+
elif [[ $branch_name == "dev" ]]; then
42+
echo "output_folder=dev" >> $GITHUB_ENV
43+
else
44+
echo "Unknown branch: $branch_name"
45+
exit 1
46+
fi
47+
48+
- name: Run tests with coverage
49+
run: |
50+
timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
51+
mkdir -p test_results
52+
log_file="test_results/${timestamp}_report.log"
53+
echo -e "\nTest Cases Report Report\n" >> $log_file
54+
# Run the tests and append output to the log file
55+
PYTHONPATH=$(pwd) python -m coverage run --source=src -m unittest discover -v tests/ >> $log_file 2>&1
56+
echo -e "\nCoverage Report\n" >> $log_file
57+
coverage report >> $log_file
58+
coverage xml
59+
60+
- name: Check coverage
61+
run: |
62+
coverage report --fail-under=85
63+
64+
- name: Upload report to Azure
65+
uses: LanceMcCarthy/Action-AzureBlobUpload@v2
66+
with:
67+
source_folder: 'test_results'
68+
destination_folder: '${{ env.output_folder }}'
69+
connection_string: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
70+
container_name: 'osw-quality-metric-service'
71+
clean_destination_folder: false
72+
delete_if_exists: false

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,5 @@ cython_debug/
162162
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
163163
#.idea/
164164
.ve
165+
MagicMock
166+
test_results

src/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ class Config(BaseSettings):
1212
incoming_topic_subscription: str = os.environ.get('QUALITY_REQ_SUB', '')
1313
outgoing_topic_name: str = os.environ.get('QUALITY_RES_TOPIC', '')
1414
storage_container_name: str = os.environ.get('CONTAINER_NAME', 'osw')
15-
algorithm_dictionary: dict = {"fixed":QMFixedCalculator,"ixn":QMXNLibCalculator}
15+
algorithm_dictionary: dict = {"fixed": QMFixedCalculator, "ixn": QMXNLibCalculator}
1616
max_concurrent_messages: int = os.environ.get('MAX_CONCURRENT_MESSAGES', 1)
17-
partition_count:int = os.environ.get('PARTITION_COUNT', 2)
17+
partition_count: int = os.environ.get('PARTITION_COUNT', 2)
1818

1919
def get_download_folder(self) -> str:
2020
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
2121
return os.path.join(root_dir, 'downloads')
22-
22+
2323
def get_assets_folder(self) -> str:
2424
root_dir = os.path.dirname(os.path.abspath(__file__))
25-
return os.path.join(root_dir, 'assets')
25+
return os.path.join(root_dir, 'assets')

src/services/servicebus_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(self) -> None:
4545

4646
def process_message(self, msg: QueueMessage):
4747
logger.info(f"Processing message {msg}")
48+
input_file_url = None
4849
try:
4950
logger.info(f"Processing message {msg.messageId}")
5051
# Parse the message

tests/calculators/__init__.py

Whitespace-only changes.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import unittest
2+
from abc import ABC, abstractmethod
3+
from src.calculators import QMCalculator
4+
from src.calculators.qm_calculator import QualityMetricResult
5+
6+
# A concrete implementation for testing purposes
7+
class MockQMCalculator(QMCalculator):
8+
def calculate_quality_metric(self) -> QualityMetricResult:
9+
return QualityMetricResult(success=True, message='Calculation succeeded.', output_file='output.geojson')
10+
11+
def algorithm_name(self) -> str:
12+
return 'mock-algorithm'
13+
14+
class TestQualityMetricResult(unittest.TestCase):
15+
def test_quality_metric_result_initialization(self):
16+
# Create a QualityMetricResult instance
17+
result = QualityMetricResult(
18+
success=True,
19+
message='Calculation succeeded.',
20+
output_file='output.geojson'
21+
)
22+
23+
# Assertions
24+
self.assertTrue(result.success)
25+
self.assertEqual(result.message, 'Calculation succeeded.')
26+
self.assertEqual(result.output_file, 'output.geojson')
27+
28+
class TestQMCalculator(unittest.TestCase):
29+
def test_qm_calculator_is_abstract(self):
30+
# Ensure QMCalculator cannot be instantiated directly
31+
with self.assertRaises(TypeError):
32+
QMCalculator()
33+
34+
def test_concrete_implementation(self):
35+
# Instantiate the mock implementation
36+
calculator = MockQMCalculator()
37+
38+
# Test calculate_quality_metric
39+
result = calculator.calculate_quality_metric()
40+
self.assertIsInstance(result, QualityMetricResult)
41+
self.assertTrue(result.success)
42+
self.assertEqual(result.message, 'Calculation succeeded.')
43+
self.assertEqual(result.output_file, 'output.geojson')
44+
45+
# Test algorithm_name
46+
self.assertEqual(calculator.algorithm_name(), 'mock-algorithm')
47+
48+
if __name__ == '__main__':
49+
unittest.main()
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import json
2+
import tempfile
3+
import unittest
4+
import geopandas as gpd
5+
from subprocess import run, PIPE
6+
from unittest.mock import patch, MagicMock
7+
from src.calculators.qm_fixed_calculator import QMFixedCalculator
8+
from src.calculators.qm_calculator import QualityMetricResult
9+
10+
11+
class TestQMFixedCalculator(unittest.TestCase):
12+
13+
def setUp(self):
14+
self.edges_file_path = 'test_edges.geojson'
15+
self.output_file_path = 'test_output.geojson'
16+
self.polygon_file_path = 'test_polygon.geojson'
17+
self.calculator = QMFixedCalculator(
18+
self.edges_file_path,
19+
self.output_file_path,
20+
self.polygon_file_path
21+
)
22+
23+
def test_initialization(self):
24+
self.assertEqual(self.calculator.edges_file_path, self.edges_file_path)
25+
self.assertEqual(self.calculator.output_file_path, self.output_file_path)
26+
self.assertEqual(self.calculator.polygon_file_path, self.polygon_file_path)
27+
28+
@patch('src.calculators.qm_fixed_calculator.gpd.read_file')
29+
@patch('src.calculators.qm_fixed_calculator.gpd.GeoDataFrame.to_file', autospec=True)
30+
@patch('src.calculators.qm_fixed_calculator.random.randint')
31+
def test_calculate_quality_metric(self, mock_randint, mock_to_file, mock_read_file):
32+
# Setup mocks
33+
mock_randint.return_value = 42
34+
mock_gdf = MagicMock(spec=gpd.GeoDataFrame)
35+
mock_read_file.return_value = mock_gdf
36+
37+
# Call the method under test
38+
result = self.calculator.calculate_quality_metric()
39+
40+
# Assertions
41+
mock_read_file.assert_called_once_with(self.edges_file_path)
42+
mock_gdf.__setitem__.assert_called_once_with('fixed_score', 42)
43+
mock_gdf.to_file.assert_called_once_with(self.output_file_path)
44+
self.assertIsInstance(result, QualityMetricResult)
45+
self.assertTrue(result.success)
46+
self.assertEqual(result.message, 'QMFixedCalculator')
47+
self.assertEqual(result.output_file, self.output_file_path)
48+
49+
def test_algorithm_name(self):
50+
self.assertEqual(self.calculator.algorithm_name(), 'QMFixedCalculator')
51+
52+
def test_main_with_polygon(self):
53+
# Create temporary files for edges and polygon
54+
with tempfile.NamedTemporaryFile(suffix='.geojson') as edges_file, \
55+
tempfile.NamedTemporaryFile(suffix='.geojson') as output_file, \
56+
tempfile.NamedTemporaryFile(suffix='.geojson') as polygon_file:
57+
# Write dummy GeoJSON data to edges and polygon files
58+
dummy_geojson = {
59+
'type': 'FeatureCollection',
60+
'features': [
61+
{
62+
'type': 'Feature',
63+
'geometry': {'type': 'LineString', 'coordinates': [[0, 0], [1, 1]]},
64+
'properties': {}
65+
}
66+
]
67+
}
68+
69+
edges_file.write(json.dumps(dummy_geojson).encode())
70+
edges_file.flush()
71+
72+
polygon_file.write(json.dumps(dummy_geojson).encode())
73+
polygon_file.flush()
74+
75+
# Simulate running the script as a standalone process
76+
result = run(
77+
[
78+
'python',
79+
'src/calculators/qm_fixed_calculator.py',
80+
edges_file.name,
81+
output_file.name,
82+
polygon_file.name,
83+
],
84+
stdout=PIPE,
85+
stderr=PIPE,
86+
text=True,
87+
)
88+
89+
# Debugging: Print stderr if test fails
90+
if result.returncode != 0:
91+
print('Error:', result.stderr)
92+
93+
# Assertions
94+
self.assertEqual(result.returncode, 0)
95+
self.assertIn('QMFixedCalculator', result.stdout)
96+
97+
def test_main_without_polygon(self):
98+
# Create temporary files for edges
99+
with tempfile.NamedTemporaryFile(suffix='.geojson') as edges_file, \
100+
tempfile.NamedTemporaryFile(suffix='.geojson') as output_file:
101+
# Write dummy GeoJSON data to edges file
102+
dummy_geojson = {
103+
'type': 'FeatureCollection',
104+
'features': []
105+
}
106+
107+
edges_file.write(json.dumps(dummy_geojson).encode())
108+
edges_file.flush()
109+
110+
# Simulate running the script as a standalone process
111+
result = run(
112+
[
113+
'python',
114+
'src/calculators/qm_fixed_calculator.py',
115+
edges_file.name,
116+
output_file.name,
117+
],
118+
stdout=PIPE,
119+
stderr=PIPE,
120+
text=True,
121+
)
122+
123+
# Assertions
124+
self.assertEqual(result.returncode, 0)
125+
self.assertIn('QMFixedCalculator', result.stdout)
126+
127+
128+
129+
if __name__ == '__main__':
130+
unittest.main()

0 commit comments

Comments
 (0)