Skip to content

Commit f52349c

Browse files
committed
Add pytests and CI. Update README to reflect new process.
1 parent eb670ad commit f52349c

File tree

5 files changed

+232
-52
lines changed

5 files changed

+232
-52
lines changed

.github/workflows/CI.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags: ['*']
8+
pull_request:
9+
workflow_dispatch:
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v5
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.11'
20+
- name: Install dependencies
21+
run: |
22+
python -m pip install --upgrade pip
23+
pip install -e .[test]
24+
- name: Test with pytest
25+
run: |
26+
pytest metoppy/tests --cov=metoppy --cov-report=xml

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ The following dependencies are only required for building/editing/testing the so
3535
| Dependency | Version | License | Home URL |
3636
|------|---------|---------|--------------|
3737
| pytest | 8.4.1 | MIT License (MIT) | https://docs.pytest.org/en/latest |
38+
| pytest-cov | 7.0 | MIT License (MIT) | https://pytest-cov.readthedocs.io/en/latest |
3839
| pytest-html | 4.1.1 | MIT License (MIT) | https://github.com/pytest-dev/pytest-html |
3940
| pytest-mock | 3.14.1 | MIT License (MIT) | https://github.com/pytest-dev/pytest-mock |
40-
| coverage | 7.10.5 | Apache Software License (Apache License Version 2.0) | https://github.com/nedbat/coveragepy |
4141
| pre-commit | 4.3.0 | MIT License (MIT) | https://github.com/pre-commit/pre-commit |
42+
| numpy | 2.3.4 | BSD License (BSD) | https://numpy.org/ |
4243

4344
## Example
4445

@@ -144,12 +145,12 @@ print(metop_reader.shape(longitude_slice))
144145

145146
</details>
146147

147-
6. Covert julia array to numpy (requires that numpy is also installed)
148+
6. View the julia array as numpy (requires that numpy is also installed)
148149

149150
```python
150151
import numpy as np
151-
# Covert the julia array as numpy
152-
longitude_slice_np = np.array(longitude_slice) # "copy = None" can be used to reduce memory
152+
# View the julia array as a numpy array
153+
longitude_slice_np = np.array(longitude_slice, copy = None)
153154
print(longitude_slice_np)
154155
```
155156
<details>
@@ -193,13 +194,13 @@ docker run -v ./:/usr/local/bin/metoppy -it python:3.12 /bin/bash
193194

194195
3. Move to the repository and install the package for testing
195196
```
196-
cd /usr/local/bin/metoppy && pip install -e .
197+
cd /usr/local/bin/metoppy && pip install -e .[test]
197198
```
198199

199200
4. Modify the local code and test in the container.
200201

201202
```
202-
python3 metoppy/tests/test_get_test_data_artifact.py
203+
pytest metoppy/tests
203204
```
204205

205-
5. When you are happy, push code to your fork and open a MR (Gitlab) or PR (Github)
206+
5. When you are happy, push code to your fork and open a PR (Github)
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env python
2+
#
3+
# Package Name: metoppy
4+
# Author: Simon Kok Lupemba, Francesco Murdaca
5+
# License: MIT License
6+
# Copyright (c) 2025 EUMETSAT
7+
8+
# This package is licensed under the MIT License.
9+
# See the LICENSE file for more details.
10+
11+
"""Test file."""
12+
13+
import pytest
14+
from pathlib import Path
15+
from metoppy.metopreader import MetopReader
16+
17+
18+
@pytest.fixture(scope="module")
19+
def metop_reader():
20+
"""
21+
Initialize the MetopReader once for the entire module
22+
"""
23+
reader = MetopReader()
24+
return reader # Make it available to tests
25+
26+
27+
@pytest.fixture
28+
def test_file(request, metop_reader):
29+
"""
30+
Fixture to get test file
31+
"""
32+
product_type = request.param # get parameter from the test
33+
34+
reduced_data_folder = Path(metop_reader.get_test_data_artifact())
35+
reduced_data_files = [f for f in reduced_data_folder.iterdir() if f.is_file()]
36+
test_file_name = next((f for f in reduced_data_files if f.name.startswith(product_type)), None)
37+
test_file_path = reduced_data_folder / test_file_name
38+
39+
return test_file_path
40+
41+
42+
@pytest.mark.parametrize("test_file", ["ASCA_SZO"], indirect=True)
43+
def test_get_keys(metop_reader, test_file):
44+
"""
45+
Simple test for metop_reader.get_key
46+
"""
47+
# arrange
48+
ds = metop_reader.open_dataset(file_path=str(test_file))
49+
50+
# act
51+
keys = metop_reader.get_keys(ds)
52+
53+
# assert
54+
assert "latitude" in keys
55+
assert "record_start_time" in keys
56+
assert "sigma0_trip" in keys
57+
assert "utc_line_nodes" in keys
58+
assert "latitude_full" not in keys
59+
60+
# clean
61+
metop_reader.close_dataset(ds)
62+
63+
64+
@pytest.mark.parametrize("test_file", ["ASCA_SZO"], indirect=True)
65+
def test_close_dataset(metop_reader, test_file):
66+
"""
67+
Test for metop_reader.test_close_dataset. It should not be
68+
possible to read from a closed dataset
69+
"""
70+
# arrange
71+
import juliacall
72+
ds = metop_reader.open_dataset(file_path=str(test_file))
73+
74+
# act
75+
metop_reader.close_dataset(ds)
76+
77+
# assert
78+
with pytest.raises(juliacall.JuliaError):
79+
ds['longitude'][0,0]
80+
81+
82+
@pytest.mark.parametrize("test_file", ["ASCA_SZO"], indirect=True)
83+
def test_shape(metop_reader, test_file):
84+
"""
85+
Simple test for metop_reader.shape.
86+
"""
87+
# arrange
88+
ds = metop_reader.open_dataset(file_path=str(test_file))
89+
90+
# act
91+
latitude = ds['latitude']
92+
longitude_slice = ds['longitude'][10:14,0:2]
93+
94+
shape_latitude = metop_reader.shape(latitude)
95+
shape_longitude_slice = metop_reader.shape(longitude_slice)
96+
97+
# assert
98+
assert shape_latitude == (42,10)
99+
assert shape_longitude_slice == (4,2)
100+
101+
# clean
102+
metop_reader.close_dataset(ds)
103+
104+
105+
@pytest.mark.parametrize("test_file", ["IASI_xxx"], indirect=True)
106+
def test_read_single_value(metop_reader, test_file):
107+
"""
108+
Test reading scalar value and assert that the value is correct.
109+
The test also checks that Julia datetimes are converted to Python datetime.datetime
110+
"""
111+
# arrange
112+
import datetime
113+
ds = metop_reader.open_dataset(file_path=str(test_file))
114+
115+
# act
116+
CO2_radiance = ds["gs1cspect"][91, 0, 0, 0]
117+
start_time = ds["record_start_time"][0]
118+
119+
# assert
120+
assert CO2_radiance == pytest.approx(0.0006165, abs=2e-5)
121+
assert isinstance(CO2_radiance, float)
122+
123+
assert start_time.year == 2024
124+
assert start_time.month == 9
125+
assert start_time.day == 25
126+
assert isinstance(start_time, datetime.datetime)
127+
128+
# clean
129+
metop_reader.close_dataset(ds)
130+
131+
@pytest.mark.parametrize("test_file", ["ASCA_SZR"], indirect=True)
132+
def test_read_array(metop_reader, test_file):
133+
"""
134+
Test reading varible as an array and conveting it to numpy.
135+
This test uses default parameter which results in less performant
136+
dynamic types.
137+
"""
138+
# arrange
139+
import numpy as np
140+
ds = metop_reader.open_dataset(file_path=str(test_file))
141+
142+
# act
143+
latitude_julia = metop_reader.as_array(ds['latitude'])
144+
longitude_julia = metop_reader.as_array(ds['longitude'])
145+
longitude_slice_julia = ds['longitude'][10:14,0:2]
146+
latitude = np.array(latitude_julia, copy = None)
147+
longitude = np.array(longitude_julia, copy = None)
148+
longitude_slice = np.array(longitude_slice_julia, copy = None)
149+
150+
# assert
151+
assert np.all((0 < longitude)&(longitude < 360))
152+
assert np.all((-90 < latitude)&(latitude < 90))
153+
assert np.all((0 < longitude_slice)&(longitude_slice < 360))
154+
assert longitude_slice.shape == (4,2)
155+
156+
# clean
157+
metop_reader.close_dataset(ds)
158+
159+
@pytest.mark.parametrize("test_file", ["ASCA_SZR"], indirect=True)
160+
def test_type_stable_array(metop_reader, test_file):
161+
"""
162+
Test reading varible as an array and conveting it to numpy the performant way.
163+
This also check that the numpy data type is set correctly.
164+
"maskingvalue = float("nan")" is used to generate arrays with concrete data type.
165+
"""
166+
# arrange
167+
import numpy as np
168+
ds = metop_reader.open_dataset(file_path=str(test_file), maskingvalue = float("nan"))
169+
170+
# act
171+
latitude = np.array(metop_reader.as_array(ds['latitude']), copy = None)
172+
longitude = np.array(metop_reader.as_array(ds['longitude']), copy = None)
173+
174+
# assert
175+
assert latitude.dtype == np.dtype('float64')
176+
assert longitude.dtype == np.dtype('float64')
177+
assert np.all((0 < longitude)&(longitude < 360))
178+
assert np.all((-90 < latitude)&(latitude < 90))
179+
180+
# clean
181+
metop_reader.close_dataset(ds)
182+
183+
@pytest.mark.parametrize("test_file", ["ASCA_SZF", "ASCA_SZO","ASCA_SZR", "MHSx_xxx", "HIRS_xxx", "AMSA_xxx", "IASI_SND", "IASI_xxx"], indirect=True)
184+
def test_different_file_types(metop_reader, test_file):
185+
"""
186+
Test that different types of test files can be opened.
187+
"""
188+
# act
189+
ds = metop_reader.open_dataset(file_path=str(test_file))
190+
191+
# assert
192+
assert ds is not None
193+
assert "record_start_time" in metop_reader.get_keys(ds)
194+
195+
# clean
196+
metop_reader.close_dataset(ds)

metoppy/tests/test_get_test_data_artifact.py

Lines changed: 0 additions & 44 deletions
This file was deleted.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ Organization = "https://github.com/eumetsat"
3737
[project.optional-dependencies]
3838
test = [
3939
"pytest==8.4.1",
40+
"pytest-cov==7.0",
4041
"pytest-html==4.1.1",
4142
"pytest-mock==3.14.1",
42-
"coverage==7.10.5",
4343
"pre-commit==4.3.0",
44+
"numpy==2.3.4",
4445
]
4546

4647
[build-system]

0 commit comments

Comments
 (0)