Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
|-----------------|------------------|-------------|------------|
| andrewcoughtrie | Andrew Coughtrie | Met Office | 2026-03-05 |
| mo-marqh | mark Hedley | Met Office | 2026-03-10 |
| oakleybrunt | Oakley Brunt | Met Office | 2026-03-09 |
36 changes: 30 additions & 6 deletions post-processing/tests/test_cli_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
# The file LICENCE, distributed with this code, contains details of the terms
# under which the code may be used.
# ------------------------------------------------------------------------------
"""
Module to hold tests for CLI tools.
"""
from pathlib import Path
import unittest
import subprocess
import sys

class TestCLITools(unittest.TestCase):

class TestCLITools(unittest.TestCase):
"""
Unittest class for testing CLI tools.
"""
def setUp(self):
"""
Initialise useful attributes for testing.
"""
self.tools_dir = Path(__file__).parent.parent / 'tools'
self.test_data_dir = Path(__file__).parent / 'data'
# pylint: disable=line-too-long
self.test_data_kgo = (
'| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |\n' +
'| __test_app__ | 6.3465 | 2.5855 | 4.0895 | 1 | 40.9825 | 6.3465 |\n' +
Expand All @@ -21,29 +30,44 @@ def setUp(self):
return super().setUp()

def test_summarise_vernier_file(self):
"""
Tests the summarise_vernier python script on a single file and
compares the output to a KGO.
"""
result = subprocess.run(
[str(self.tools_dir / 'summarise_vernier.py'),
str(self.test_data_dir / 'vernier-output-test')],
capture_output=True,
text=True
text=True,
check=False
)
self.assertEqual(result.returncode, 0)
self.assertEqual(self.test_data_kgo, result.stdout)

def test_summarise_vernier_dir(self):
"""
Tests the summarise_vernier python script on a directory and compares
the output to a KGO.
"""
result = subprocess.run(
[str(self.tools_dir / 'summarise_vernier.py'),
str(self.test_data_dir / 'vernier-output')],
capture_output=True,
text=True
text=True,
check=False
)
self.assertEqual(result.returncode, 0)
self.assertEqual(self.test_data_kgo, result.stdout)

def test_summarise_vernier_noinput(self):
"""
Tests that the correct error is raised when calling the
summarise_vernier python script on an empty file.
"""
result = subprocess.run(
[str(self.tools_dir / 'summarise_vernier.py')],
capture_output=True,
text=True
text=True,
check=False
)
self.assertEqual(result.returncode, 2)
self.assertEqual(result.returncode, 2)
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,34 @@
# The file LICENCE, distributed with this code, contains details of the terms
# under which the code may be used.
# ------------------------------------------------------------------------------
"""
Module for testing the VernierCalliper class.
"""

import unittest
from pathlib import Path
import sys

# pylint: disable=wrong-import-position
sys.path.append(str(Path(__file__).parent.parent))
from vernier.vernier_data import VernierCalliper

class TestVernierCalliper(unittest.TestCase):

class TestVernierCalliper(unittest.TestCase):
"""
Unittest class for holding tests related to the VernierCalliper class.
"""
def setUp(self):
"""
Initialise useful attributes for testing.
"""
self.calliper_a = VernierCalliper("test_calliper_a")
self.calliper_b = VernierCalliper("test_calliper_b")

def test_init(self):
"""
Test that the class initialises as expected (empty lists)
"""
self.assertEqual(self.calliper_a.name, "test_calliper_a")
self.assertEqual(self.calliper_a.time_percent, [])
self.assertEqual(self.calliper_a.cumul_time, [])
Expand All @@ -24,6 +39,10 @@ def test_init(self):
self.assertEqual(self.calliper_a.n_calls, [])

def test_reduce(self):
"""
Test that the reduce() method returns the expected results.
"""
# pylint: disable=pointless-statement
self.calliper_a.time_percent = [10.0, 20.0]
self.calliper_a.cumul_time = [30.0, 40.0]
self.calliper_a.self_time = [5.0, 15.0]
Expand All @@ -40,6 +59,11 @@ def test_reduce(self):
self.assertEqual(reduced_data[6], 15.0)

def test_compare(self):
"""
Test that the comparison of dataclasses works as expected for the
VernierCalliper class.
"""
# pylint: disable=pointless-statement
self.calliper_a.time_percent = [10.0, 20.0]
self.calliper_a.cumul_time = [30.0, 40.0]
self.calliper_a.self_time = [5.0, 15.0]
Expand All @@ -56,5 +80,6 @@ def test_compare(self):
self.assertFalse(self.calliper_a > self.calliper_b)
self.assertFalse(self.calliper_a == self.calliper_b)


if __name__ == '__main__':
unittest.main()
102 changes: 94 additions & 8 deletions post-processing/tests/test_vernier_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,35 @@
# The file LICENCE, distributed with this code, contains details of the terms
# under which the code may be used.
# ------------------------------------------------------------------------------
"""
Module to hold tests for VernierData class and aggregate function.
"""
from pathlib import Path
from io import StringIO
import tempfile
import unittest
import sys

# pylint: disable=wrong-import-position
sys.path.append(str(Path(__file__).parent.parent))
from vernier import VernierData, VernierDataCollation


class TestVernierData(unittest.TestCase):
"""
Tests for the VernierData class and aggregator function
Tests for the VernierData class and aggregator function.
"""
def setUp(self):
"""
Initialise useful attributes for testing.
"""
self.test_data = VernierData()

def test_add_empty_calliper(self):
"""
Tests that a VernierCalliper instance is correctly added to the
VernierData instance.
"""
self.test_data.add_calliper("test_calliper")
self.assertIn("test_calliper", self.test_data.data)
self.assertEqual(self.test_data.data["test_calliper"].time_percent, [])
Expand All @@ -28,44 +41,70 @@ def test_add_empty_calliper(self):
self.assertEqual(self.test_data.data["test_calliper"].n_calls, [])

def test_filter_calliper(self):
"""
Tests that callipers that do not match the pattern pased to
`VernierData.filter()` are not returned.
"""
self.test_data.add_calliper("timestep_calliper")
self.test_data.add_calliper("other_calliper")
filtered = self.test_data.filter(["timestep"])
self.assertIn("timestep_calliper", filtered.data)
self.assertNotIn("other_calliper", filtered.data)

def test_filter_no_match(self):
"""
Test that an error is raised when no callipers are found when calling
the `VernierData.filter()` method.
"""
self.test_data.add_calliper("timestep_calliper")
with self.assertRaises(ValueError):
self.test_data.filter(["nonexistent"])

def test_filter_multiple_matches(self):
"""
Tests that filter correctly returns all callipers with a substring
equal to the pattern passed to `VernierData.filter()`
"""
self.test_data.add_calliper("timestep_calliper_1")
self.test_data.add_calliper("timestep_calliper_2")
filtered = self.test_data.filter(["timestep"])
self.assertIn("timestep_calliper_1", filtered.data)
self.assertIn("timestep_calliper_2", filtered.data)

def test_filter_empty_keys(self):
"""
Test that an error is raised when no filters are passed to the
`VernierData.filter()` method.
"""
self.test_data.add_calliper("timestep_calliper")
with self.assertRaises(ValueError):
self.test_data.filter([])

def test_write_txt_output_file(self):
"""
Test that the formatting of write_txt_ouput is as expected when writing
to a file.
"""
self.test_data.add_calliper("test_calliper")
self.test_data.data["test_calliper"].time_percent = [10.0, 20.0]
self.test_data.data["test_calliper"].cumul_time = [30.0, 40.0]
self.test_data.data["test_calliper"].self_time = [5.0, 15.0]
self.test_data.data["test_calliper"].total_time = [25.0, 35.0]
self.test_data.data["test_calliper"].n_calls = [2]

# pylint: disable=unspecified-encoding
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
self.test_data.write_txt_output(Path(tmp_file.name))
contents = Path(tmp_file.name).read_text().splitlines()
# pylint: disable=line-too-long
self.assertEqual("| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |", contents[0])
self.assertEqual("| test_calliper | 30.0 | 10.0 | 35.0 | 2 | 15.0 | 15.0 |", contents[1])

def test_write_txt_output_terminal(self):
"""
Test that the formatting of write_txt_ouput is as expected when writing
to the terminal.
"""
self.test_data.add_calliper("test_calliper")
self.test_data.data["test_calliper"].time_percent = [50.0, 40.0]
self.test_data.data["test_calliper"].cumul_time = [10.0, 12.0]
Expand All @@ -78,10 +117,15 @@ def test_write_txt_output_terminal(self):
self.test_data.write_txt_output()
sys.stdout = sys.__stdout__

# pylint: disable=line-too-long
self.assertEqual("| Routine | Total time (s) | Self (s) | Cumul time (s) | No. calls | % time | Time per call (s) |", write_output.getvalue().splitlines()[0])
self.assertEqual("| test_calliper | 35.0 | 3.5 | 11.0 | 2 | 45.0 | 17.5 |", write_output.getvalue().splitlines()[1])

def test_aggregate(self):
"""
Tests that aggregate returns a single VernierData object with all
expected callipers and data.
"""
data1 = VernierData()
data1.add_calliper("calliper_a")
data1.data["calliper_a"].time_percent = [10.0, 20.0]
Expand All @@ -101,13 +145,28 @@ def test_aggregate(self):
aggregated = VernierData()
aggregated.aggregate([data1, data2])
self.assertIn("calliper_a", aggregated.data)
self.assertEqual(aggregated.data["calliper_a"].time_percent, [10.0, 20.0, 15.0, 25.0])
self.assertEqual(aggregated.data["calliper_a"].cumul_time, [30.0, 40.0, 35.0, 45.0])
self.assertEqual(aggregated.data["calliper_a"].self_time, [5.0, 15.0, 6.0, 16.0])
self.assertEqual(aggregated.data["calliper_a"].total_time, [25.0, 35.0, 28.0, 38.0])
self.assertEqual(aggregated.data["calliper_a"].n_calls, [2, 2, 3, 3])
self.assertEqual(
aggregated.data["calliper_a"].time_percent,
[10.0, 20.0, 15.0, 25.0])
self.assertEqual(
aggregated.data["calliper_a"].cumul_time,
[30.0, 40.0, 35.0, 45.0])
self.assertEqual(
aggregated.data["calliper_a"].self_time,
[5.0, 15.0, 6.0, 16.0])
self.assertEqual(
aggregated.data["calliper_a"].total_time,
[25.0, 35.0, 28.0, 38.0])
self.assertEqual(
aggregated.data["calliper_a"].n_calls,
[2, 2, 3, 3])

def test_aggregate_inconsistent(self):
"""
Tests that an error is raised when VernierData objects with mismatched
callipers are passed to aggregate with internal consistency constraints
turned on.
"""
data1 = VernierData()
data1.add_calliper("calliper_a")
data1.data["calliper_a"].time_percent = [10.0, 20.0]
Expand All @@ -129,6 +188,10 @@ def test_aggregate_inconsistent(self):
aggregated.aggregate([data1, data2])

def test_aggregate_inconsistent_ok(self):
"""
Test that combining VernierData objects without forced consistency
of callipers works as expected (both callipers present in output).
"""
data1 = VernierData()
data1.add_calliper("calliper_a")

Expand All @@ -141,6 +204,9 @@ def test_aggregate_inconsistent_ok(self):
self.assertIn("calliper_b", aggregated.data)

def test_get(self):
"""
Test that the get method of the VernierData class works as expected.
"""
data1 = VernierData()
data1.add_calliper("calliper_a")
data1.data["calliper_a"].time_percent = [10.0, 20.0]
Expand All @@ -156,6 +222,9 @@ class TestVernierCollation(unittest.TestCase):
Tests for the VernierData Collation class.
"""
def _add_data(self):
"""
Setup for testing the VernierCollation object.
"""
self.collation = VernierDataCollation()
data1 = VernierData()
data1.add_calliper("calliper_a")
Expand All @@ -175,22 +244,38 @@ def _add_data(self):

self.collation.add_data('test1', data1)
self.collation.add_data('test2', data2)

def test_add_data(self):
"""
Test that the add_data method adds VernierData objects to the collation
attribute.
"""
self._add_data()
self.assertEqual(len(self.collation), 2)

def test_remove_data(self):
"""
Test that the remove_data method drops the correct VernierData object
from the collation attribute.
"""
self._add_data()
self.collation.remove_data('test1')
self.assertEqual(len(self.collation), 1)

def test_get(self):
"""
Test that the get method of VernierCollation returns the expected
VernierData instance.
"""
self._add_data()
calliper_a = self.collation.get("calliper_a")
self.assertEqual(len(calliper_a), 4)

def test_internal_consistency(self):
"""
Test that the internal_consistency method of VernierCollation returns
the expected error type and message.
"""
self._add_data()
data_inc = VernierData()
data_inc.add_calliper("calliper_a")
Expand All @@ -210,7 +295,8 @@ def test_internal_consistency(self):
with self.assertRaises(ValueError) as test_exception:
self.collation.add_data('test3', data_inc)
self.assertEqual(str(test_exception.exception),
"inconsistent callipers in new_vernier_data")
"Inconsistent callipers in new_vernier_data: "
"['calliper_a', 'calliper_b'] detected as unmatched")


if __name__ == '__main__':
Expand Down
Loading
Loading