Skip to content

Commit d8eaf55

Browse files
authored
Use GitHub actions for unit testing (#745)
* Add GitHub Actions and Dockerfile for test environment * Fix env var query in github workflow * Revert "Fix env var query in github workflow" This reverts commit dc37cec. * Try specifying Docker tag with variables * Hardcode Docker Hub namespace * Conditionally run jobs and try to fix unittest job * Fix syntax error in github workflow * Do not call make in github workflow * Try fixing the shell inside Docker container for github workflow * Call micromamba directly in github workflow * Use 'micromamba install' because 'update' does not update pip deps * Split GitHub workflows * Consolidate GitHub Workflows * Try fixing syntax for GitHub workflow expression * Fix typo in docker workflow * Fix list input for docker.yml workflow * Run all tests in GitHub workflow * Do not use 'defaults' in workflow * Fix stray 'defaults' in workflow * Rename testing steps * Do not use multiprocessing for integration tests * Revert climada.conf to 9302bf2 * Revert "Do not use multiprocessing for integration tests" This reverts commit 9be8632. The command does not actually use multiprocessing but makes coverage aware of multiprocessing used by the called tests. * Skip failing integration tests for now * Add action to publish test results * Run pipeline on branches and pull requests * Grant permissions from top-level job * Use other XML test reports for displaying results * Add new workflow using micromamba directly * Add data and notebook tests to new workflow * Separately build conda environment This avoids building a new environment for every job. * Add integration tests and separate test result reporting * Fix missing 'runs-on' * ci: Add ipython to extra requirements * ci: Fix publish results * Fix permissions. * Fix path to artifacts. * Remove all but the mamba pipelines * Run test matrix with different Python versions Remove version pip for Python in environment specs. * Fix Python version parsing in CI * Add linting job to CI * Skip integration tests in GitHub CI * Use custom conda environment cache key in CI * Add matchers and custom linting step to CI * Ignore pylint errors in CI * ci: Debug matcher path * ci: Lint with ruff * ci: Fix Python target version for ruff * ci: Add permissions for publishing checks to test job * Revert "ci: Lint with ruff" This reverts commits 41bb4c2 and 60ee28f. * Update CI configuration * Use new micromamba action. * Do not lint. * Upload coverage reports as artifacts. * Report test results for every Python version individually. * Stop testing Python 3.11 due to incompatibilities with dataclass * Revert "Stop testing Python 3.11 due to incompatibilities with dataclass" This reverts commit 6ed83e6. * Set environment caching for GitHub workflow * Remove unused files * Fix mutable default values in ImpactFreqCurve * Fix Nightlight and LitPop tests * Move file checks to integration tests. They require file downloads. * Fix assertion for almost equal arrays. * Fix accuracy in default Emanuel impact func test * Improve names in Github CI * Disallow Python 3.11 for Climada The unsequa module throws segmentation faults while unit testing. * Try testing Python v3.11 again * Improve GitHub Actions documentation * Add introduction to GitHub Actions to the docs. * Add information to the CI guide. * Improve comments in the ci.yml workflow definition.
1 parent 36953fc commit d8eaf55

File tree

11 files changed

+189
-73
lines changed

11 files changed

+189
-73
lines changed

.github/workflows/ci.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: GitHub CI
2+
3+
# Execute this for every push
4+
on: [push]
5+
6+
# Use bash explicitly for being able to enter the conda environment
7+
defaults:
8+
run:
9+
shell: bash -l {0}
10+
11+
jobs:
12+
build-and-test:
13+
name: Build Env, Install, Unit Tests
14+
runs-on: ubuntu-latest
15+
permissions:
16+
# For publishing results
17+
checks: write
18+
19+
# Run this test for different Python versions
20+
strategy:
21+
# Do not abort other tests if only a single one fails
22+
fail-fast: false
23+
matrix:
24+
python-version: ["3.9", "3.10", "3.11"]
25+
26+
steps:
27+
-
28+
name: Checkout Repo
29+
uses: actions/checkout@v3
30+
-
31+
# Store the current date to use it as cache key for the environment
32+
name: Get current date
33+
id: date
34+
run: echo "date=$(date +%Y-%m-%d)" >> "${GITHUB_OUTPUT}"
35+
-
36+
name: Create Environment with Mamba
37+
uses: mamba-org/setup-micromamba@v1
38+
with:
39+
environment-name: climada_env_${{ matrix.python-version }}
40+
environment-file: requirements/env_climada.yml
41+
create-args: >-
42+
python=${{ matrix.python-version }}
43+
make
44+
init-shell: >-
45+
bash
46+
# Persist environment for branch, Python version, single day
47+
cache-environment-key: env-${{ github.ref }}-${{ matrix.python-version }}-${{ steps.date.outputs.date }}
48+
-
49+
name: Install CLIMADA
50+
run: |
51+
python -m pip install ".[test]"
52+
-
53+
name: Run Unit Tests
54+
run: |
55+
make unit_test
56+
-
57+
name: Publish Test Results
58+
uses: EnricoMi/publish-unit-test-result-action@v2
59+
if: always()
60+
with:
61+
junit_files: tests_xml/tests.xml
62+
check_name: "Unit Test Results Python ${{ matrix.python-version }}"
63+
comment_mode: "off"
64+
-
65+
name: Upload Coverage Reports
66+
if: always()
67+
uses: actions/upload-artifact@v3
68+
with:
69+
name: coverage-report-unittests-py${{ matrix.python-version }}
70+
path: coverage/

climada/engine/impact.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
__all__ = ['ImpactFreqCurve', 'Impact']
2323

24-
from dataclasses import dataclass
24+
from dataclasses import dataclass, field
2525
import logging
2626
import copy
2727
import csv
@@ -1785,10 +1785,10 @@ class ImpactFreqCurve():
17851785
"""Impact exceedence frequency curve.
17861786
"""
17871787

1788-
return_per : np.array = np.array([])
1788+
return_per : np.ndarray = field(default_factory=lambda: np.empty(0))
17891789
"""return period"""
17901790

1791-
impact : np.array = np.array([])
1791+
impact : np.ndarray = field(default_factory=lambda: np.empty(0))
17921792
"""impact exceeding frequency"""
17931793

17941794
unit : str = ''

climada/entity/exposures/test/test_litpop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def test_gridpoints_core_calc_offsets_exp_rescale(self):
317317
self.assertEqual(result_array.shape, results_check.shape)
318318
self.assertAlmostEqual(result_array.sum(), tot)
319319
self.assertEqual(result_array[1,2], results_check[1,2])
320-
np.testing.assert_array_almost_equal_nulp(result_array, results_check)
320+
np.testing.assert_allclose(result_array, results_check)
321321

322322
def test_grp_read_pass(self):
323323
"""test _grp_read() to pass and return either dict with admin1 values or None"""

climada/entity/exposures/test/test_nightlight.py

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,6 @@ def test_required_files(self):
5656
self.assertRaises(ValueError, nightlight.get_required_nl_files,
5757
(-90, 90))
5858

59-
def test_check_files_exist(self):
60-
"""Test check_nightlight_local_file_exists"""
61-
# If invalid directory is supplied it has to fail
62-
try:
63-
nightlight.check_nl_local_file_exists(
64-
np.ones(np.count_nonzero(BM_FILENAMES)), 'Invalid/path')[0]
65-
raise Exception("if the path is not valid, check_nl_local_file_exists should fail")
66-
except ValueError:
67-
pass
68-
files_exist = nightlight.check_nl_local_file_exists(
69-
np.ones(np.count_nonzero(BM_FILENAMES)), SYSTEM_DIR)
70-
self.assertTrue(
71-
files_exist.sum() > 0,
72-
f'{files_exist} {BM_FILENAMES}'
73-
)
74-
7559
def test_download_nightlight_files(self):
7660
"""Test check_nightlight_local_file_exists"""
7761
# Not the same length of arguments
@@ -118,42 +102,6 @@ def test_get_required_nl_files(self):
118102
bool = np.array_equal(np.array([0, 0, 0, 0, 0, 0, 1, 0]), req_files)
119103
self.assertTrue(bool)
120104

121-
def test_check_nl_local_file_exists(self):
122-
""" Test that an array with the correct number of already existing files
123-
is produced, the LOGGER messages logged and the ValueError raised. """
124-
125-
# check logger messages by giving a to short req_file
126-
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level='WARNING') as cm:
127-
nightlight.check_nl_local_file_exists(required_files = np.array([0, 0, 1, 1]))
128-
self.assertIn('The parameter \'required_files\' was too short and is ignored',
129-
cm.output[0])
130-
131-
# check logger message: not all files are available
132-
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level='DEBUG') as cm:
133-
nightlight.check_nl_local_file_exists()
134-
self.assertIn('Not all satellite files available. Found ', cm.output[0])
135-
self.assertIn(f' out of 8 required files in {Path(SYSTEM_DIR)}', cm.output[0])
136-
137-
# check logger message: no files found in checkpath
138-
check_path = Path('climada/entity/exposures')
139-
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level='INFO') as cm:
140-
# using a random path where no files are stored
141-
nightlight.check_nl_local_file_exists(check_path=check_path)
142-
self.assertIn(f'No satellite files found locally in {check_path}',
143-
cm.output[0])
144-
145-
# test raises with wrong path
146-
check_path = Path('/random/wrong/path')
147-
with self.assertRaises(ValueError) as cm:
148-
nightlight.check_nl_local_file_exists(check_path=check_path)
149-
self.assertEqual(f'The given path does not exist: {check_path}',
150-
str(cm.exception))
151-
152-
# test that files_exist is correct
153-
files_exist = nightlight.check_nl_local_file_exists()
154-
self.assertGreaterEqual(int(sum(files_exist)), 3)
155-
self.assertLessEqual(int(sum(files_exist)), 8)
156-
157105
# Execute Tests
158106
if __name__ == "__main__":
159107
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestNightLight)

climada/entity/impact_funcs/test/test_tc.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,30 @@ def test_default_values_pass(self):
3939
self.assertTrue(np.array_equal(imp_fun.intensity, np.arange(0, 121, 5)))
4040
self.assertTrue(np.array_equal(imp_fun.paa, np.ones((25,))))
4141
self.assertTrue(np.array_equal(imp_fun.mdd[0:6], np.zeros((6,))))
42-
self.assertTrue(np.array_equal(imp_fun.mdd[6:10],
43-
np.array([0.0006753419543492556, 0.006790495604105169,
44-
0.02425254393374475, 0.05758706257339458])))
45-
self.assertTrue(np.array_equal(imp_fun.mdd[10:15],
46-
np.array([0.10870556455111065, 0.1761433569521351,
47-
0.2553983618763961, 0.34033822528795565,
48-
0.4249447743109498])))
49-
self.assertTrue(np.array_equal(imp_fun.mdd[15:20],
50-
np.array([0.5045777092933046, 0.576424302849412,
51-
0.6393091739184916, 0.6932203123193963,
52-
0.7388256596555696])))
53-
self.assertTrue(np.array_equal(imp_fun.mdd[20:25],
54-
np.array([0.777104531116526, 0.8091124649261859,
55-
0.8358522190681132, 0.8582150905529946,
56-
0.8769633232141456])))
42+
np.testing.assert_allclose(
43+
imp_fun.mdd[6:25],
44+
[
45+
0.0006753419543492556,
46+
0.006790495604105169,
47+
0.02425254393374475,
48+
0.05758706257339458,
49+
0.10870556455111065,
50+
0.1761433569521351,
51+
0.2553983618763961,
52+
0.34033822528795565,
53+
0.4249447743109498,
54+
0.5045777092933046,
55+
0.576424302849412,
56+
0.6393091739184916,
57+
0.6932203123193963,
58+
0.7388256596555696,
59+
0.777104531116526,
60+
0.8091124649261859,
61+
0.8358522190681132,
62+
0.8582150905529946,
63+
0.8769633232141456,
64+
],
65+
)
5766

5867
def test_values_pass(self):
5968
"""Compute mdr interpolating values."""

climada/test/test_nightlight.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,58 @@ def test_untar_noaa_stable_nighlight(self):
254254
self.assertIn('found more than one potential intensity file in', cm.output[0])
255255
path_tar.unlink()
256256

257+
def test_check_nl_local_file_exists(self):
258+
""" Test that an array with the correct number of already existing files
259+
is produced, the LOGGER messages logged and the ValueError raised. """
260+
261+
# check logger messages by giving a to short req_file
262+
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level='WARNING') as cm:
263+
nightlight.check_nl_local_file_exists(required_files = np.array([0, 0, 1, 1]))
264+
self.assertIn('The parameter \'required_files\' was too short and is ignored',
265+
cm.output[0])
266+
267+
# check logger message: not all files are available
268+
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level='DEBUG') as cm:
269+
nightlight.check_nl_local_file_exists()
270+
self.assertIn('Not all satellite files available. Found ', cm.output[0])
271+
self.assertIn(f' out of 8 required files in {Path(SYSTEM_DIR)}', cm.output[0])
272+
273+
# check logger message: no files found in checkpath
274+
check_path = Path('climada/entity/exposures')
275+
with self.assertLogs('climada.entity.exposures.litpop.nightlight', level='INFO') as cm:
276+
# using a random path where no files are stored
277+
nightlight.check_nl_local_file_exists(check_path=check_path)
278+
self.assertIn(f'No satellite files found locally in {check_path}',
279+
cm.output[0])
280+
281+
# test raises with wrong path
282+
check_path = Path('/random/wrong/path')
283+
with self.assertRaises(ValueError) as cm:
284+
nightlight.check_nl_local_file_exists(check_path=check_path)
285+
self.assertEqual(f'The given path does not exist: {check_path}',
286+
str(cm.exception))
287+
288+
# test that files_exist is correct
289+
files_exist = nightlight.check_nl_local_file_exists()
290+
self.assertGreaterEqual(int(sum(files_exist)), 3)
291+
self.assertLessEqual(int(sum(files_exist)), 8)
292+
293+
def test_check_files_exist(self):
294+
"""Test check_nightlight_local_file_exists"""
295+
# If invalid directory is supplied it has to fail
296+
try:
297+
nightlight.check_nl_local_file_exists(
298+
np.ones(np.count_nonzero(BM_FILENAMES)), 'Invalid/path')[0]
299+
raise Exception("if the path is not valid, check_nl_local_file_exists should fail")
300+
except ValueError:
301+
pass
302+
files_exist = nightlight.check_nl_local_file_exists(
303+
np.ones(np.count_nonzero(BM_FILENAMES)), SYSTEM_DIR)
304+
self.assertTrue(
305+
files_exist.sum() > 0,
306+
f'{files_exist} {BM_FILENAMES}'
307+
)
308+
257309
# Execute Tests
258310
if __name__ == "__main__":
259311
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestNightlight)

doc/guide/Guide_Continuous_Integration_and_Testing.ipynb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,12 @@
299299
"\n",
300300
"- All tests must pass before submitting a pull request.\n",
301301
"- Integration tests don't run on feature branches in Jenkins, therefore developers are requested to run them locally.\n",
302-
"- After a pull request was accepted and the changes are merged to the develop branch, integration tests may still fail there and have to be addressed."
302+
"- After a pull request was accepted and the changes are merged to the develop branch, integration tests may still fail there and have to be addressed.\n",
303+
"\n",
304+
"#### GitHub Actions\n",
305+
"\n",
306+
"We adopted test automation via GitHub Actions in an experimental state.\n",
307+
"See [GitHub Actions CI](github-actions.rst) for details."
303308
]
304309
},
305310
{

doc/guide/github-actions.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
=================
2+
GitHub Actions CI
3+
=================
4+
5+
CLIMADA has been using a private Jenkins instance for automated testing (Continuous Integration, CI), see :doc:`Guide_Continuous_Integration_and_Testing`.
6+
We recently adopted `GitHub Actions <https://docs.github.com/en/actions>`_ for automated unit testing.
7+
GitHub Actions is a service provided by GitHub, which lets you configure CI/CD pipelines based on YAML configuration files.
8+
GitHub provides servers which ample computational resources to create software environments, install software, test it, and deploy it.
9+
See the `GitHub Actions Overview <https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions>`_ for a technical introduction, and the `Workflow Syntax <https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions>`_ for a reference of the pipeline definitions.
10+
11+
The CI results for each pull request can be inspected in the "Checks" tab.
12+
For GitHub Actions, users can inspect the logs of every step for every job.
13+
14+
.. note::
15+
16+
As of CLIMADA v4.0, the default CI technology remains Jenkins.
17+
GitHub Actions CI is currently considered experimental for CLIMADA development.
18+
19+
---------------------
20+
Unit Testing Pipeline
21+
---------------------
22+
23+
This pipeline is defined by the ``.github/workflows/ci.yml`` file.
24+
It contains a single job which will create a CLIMADA environment with Mamba for multiple Python versions, install CLIMADA, run the unit tests, and report the test coverage as well as the simplified test results.
25+
The job has a `strategy <https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategy>`_ which runs it for multiple times for different Python versions.
26+
This way, we make sure that CLIMADA is compatible with all currently supported versions of Python.
27+
28+
The coverage reports in HTML format will be uploaded as job artifacts and can be downloaded as ZIP files.
29+
The test results are simple testing summaries that will appear as individual checks/jobs after the respective job completed.

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Jump right in:
103103
Performance and Best Practices <guide/Guide_Py_Performance>
104104
Coding Conventions <guide/Guide_Miscellaneous>
105105
Building the Documentation <README>
106+
guide/github-actions
106107

107108

108109
.. toctree::

requirements/env_climada.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies:
2626
- pycountry>=22.3
2727
- pyepsg>=0.4
2828
- pytables>=3.7
29-
- python=3.9
29+
- python>=3.9,<3.12
3030
- pyxlsb>=1.0
3131
- rasterio>=1.3
3232
- requests>=2.31

0 commit comments

Comments
 (0)