Skip to content

Commit b34742d

Browse files
authored
Update um_fields_subset_test.py
Added Full unit tests for um_field_subset.py
1 parent 279ea28 commit b34742d

File tree

1 file changed

+196
-21
lines changed

1 file changed

+196
-21
lines changed

tests/um_fields_subset_test.py

+196-21
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,212 @@
11
import pytest
2-
from um_fields_subset_mule import parse_arguments
2+
from copy import deepcopy
3+
from unittest.mock import patch, MagicMock
4+
from um_fields_subset_mule import (parse_arguments, validate_arguments, create_default_outname, filter_fields, check_packed_fields, append_fields, PROG_STASH_CODES, MASK_CODE,void_validation)
5+
from hypothesis import given, strategies as st
6+
import numpy as np
37

8+
def test_parse_arguments_all_arguments():
9+
"""Test with all arguments provided."""
10+
test_args = ["script_name", "test_input_file", "-o", "output_file", "-p", "-v", "1,2,3", "-x", "4,5"]
11+
with patch("sys.argv", test_args):
12+
args = parse_arguments()
13+
assert args.ifile == "test_input_file"
14+
assert args.output_path == "output_file"
15+
assert args.prognostic
16+
assert args.include_list == [1, 2, 3]
17+
assert args.exclude_list == [4, 5]
418

5-
@pytest.mark.parametrize(
6-
"args, expected",
19+
def test_parse_arguments_missing_include_or_exclude():
20+
"""Test that an error is raised if neither -v nor -x is specified."""
21+
test_args = ["script_name", "test_input_file"]
22+
with patch("sys.argv", test_args):
23+
with pytest.raises(Exception):
24+
parse_arguments()
25+
26+
def test_validate_arguments_exclusion_inclusion_conflict():
27+
"""Test that -v and -x cannot be used together."""
28+
with pytest.raises(Exception):
29+
validate_arguments([1, 2], [3], False)
30+
31+
def test_validate_arguments_prognostic_and_explicit_lists():
32+
"""Test that -p cannot be used with explicit lists."""
33+
with pytest.raises(Exception):
34+
validate_arguments([1, 2], [], True)
35+
@pytest.mark.parametrize(
36+
# description of the arguments
37+
"existing_files, filename, expected_output",
738
[
8-
(["-i", "input.um", "-o", "output.um", "-v", "1,2,3"],
9-
{"ifile": "input.um", "ofile": "output.um", "vlist": [1, 2, 3], "xlist": [], "nfields": 9999999999, "prognostic": False, "section": False, "validate": False}),
10-
(["-i", "input.um", "-o", "output.um", "-x", "4,5", "--validate"],
11-
{"ifile": "input.um", "ofile": "output.um", "vlist": [], "xlist": [4, 5], "nfields": 9999999999, "prognostic": False, "section": False, "validate": True}),
12-
(["-i", "input.um", "-o", "output.um", "-n", "10", "-p", "-s", "-x", "7,8"],
13-
{"ifile": "input.um", "ofile": "output.um", "vlist": [], "xlist": [7, 8], "nfields": 10, "prognostic": True, "section": True, "validate": False})
14-
]
39+
# Case 1: Filename with suffix doesn't exist, return filename with suffix
40+
([], "testfilename", "testfilename_subset"),
41+
# Case 2: Filename with suffix exists, returns filename with suffix appending 1
42+
(["testfilename_subset"], "testfilename", "testfilename_subset1"),
43+
# Case 3: Filename with suffix and a few numbered versions exist, returns
44+
# filename with suffix and the first numbered version that doesn't exist
45+
(
46+
["testfilename_subset", "testfilename_subset1", "testfilename_subset2"],
47+
"testfilename",
48+
"testfilename_subset3",
49+
),
50+
],
51+
ids=[
52+
"file_do_not_exist",
53+
"file_exists",
54+
"multiple_files_exist",
55+
],
1556
)
57+
@patch("os.path.exists")
58+
def test_create_default_outname_suffix_not_passed(mock_exists, existing_files, filename, expected_output):
59+
"""
60+
Test the function that creates the default output file name, without passing a suffix.
61+
3 cases tested with pytest.mark.parametrize.
62+
"""
63+
# Mock os.path.exists to simulate the presence of specific files
64+
mock_exists.side_effect = lambda f: f in existing_files
65+
result = create_default_outname(filename)
66+
assert result == expected_output
67+
68+
69+
@patch("os.path.exists")
70+
def test_create_default_outname_suffix_passed(mock_exists):
71+
"""
72+
Test the function that creates the default output file name, passing a custom suffix.
73+
"""
74+
# Mock os.path.exists to simulate the presence of specific files
75+
mock_exists.return_value = False
76+
filename = "testfilename"
77+
suffix = "testsuffix"
78+
result = create_default_outname(filename, suffix)
79+
expected_output = "testfilenametestsuffix"
80+
assert result == expected_output
81+
82+
# Define a strategy for generating the list of field codes
83+
def list_strategy():
84+
return st.lists(st.integers(min_value=1, max_value=32), min_size=0, max_size=10)
85+
86+
# Define a strategy to create mock fields
87+
def field_strategy():
88+
return st.builds( lambda stash, lbpack, lblev, lbuser4: MagicMock(stash=stash, lbpack=lbpack, lblev=lblev, lbuser4=lbuser4),
89+
stash=st.integers(min_value=1, max_value=36), # stash values range
90+
lbpack=st.integers(min_value=1, max_value=4),
91+
lblev=st.integers(min_value=1, max_value=4),
92+
lbuser4=st.integers(min_value=0, max_value=36) # lbuser4 values range (updated for prognostic codes)
93+
)
94+
95+
# Define a strategy for a list of fields
96+
def field_list_strategy():
97+
return st.lists(field_strategy(), min_size=1, max_size=10)
98+
99+
@given(
100+
input_fields=field_list_strategy(),
101+
prognostic=st.booleans(),
102+
include_list=list_strategy(),
103+
exclude_list=list_strategy(),
104+
)
105+
def test_filter_fields(input_fields, prognostic, include_list, exclude_list):
106+
"""
107+
Test the filter_fields function with various input combinations.
108+
"""
109+
# Mock the input_file with fields
110+
input_file = MagicMock()
111+
input_file.fields = input_fields
16112

113+
# Call the function being tested
114+
filtered_fields = filter_fields(input_file, prognostic, include_list, exclude_list)
17115

116+
# Assert the logic
117+
for field in input_fields:
118+
if field.stash in exclude_list:
119+
assert field not in filtered_fields # Excluded fields must NOT be in the result
120+
elif include_list and field.stash in include_list:
121+
assert field in filtered_fields # Included fields must be in the result
122+
elif prognostic and field.lbuser4 in PROG_STASH_CODES:
123+
assert field in filtered_fields # Prognostic fields must be in the result if prognostic=True
124+
elif not prognostic and not include_list and not exclude_list:
125+
assert field in filtered_fields # All fields should be included if no filters are applied
126+
else:
127+
assert field not in filtered_fields # Otherwise, the field should NOT be in the result
18128

19-
def test_parse_arguments_valid(args, expected, monkeypatch):
129+
@given(field_list=field_list_strategy(),)
130+
131+
def test_check_packed_fields(field_list):
20132
"""
21-
Test parse_arguments function with valid inputs.
133+
Test the check_packed_fields function with different scenarios.
22134
"""
23-
monkeypatch.setattr("sys.argv", ["script_name"] + args)
24-
parsed_args = parse_arguments()
25-
for key, value in expected.items():
26-
assert getattr(parsed_args, key) == value
135+
MASK_CODE = 30 # Define the MASK_CODE constant for testing
136+
137+
needmask = False
138+
masked = False
139+
140+
for field in field_list:
141+
if field.lbpack == 2 and field.lblev in (1,2):
142+
needmask = True
143+
144+
if field.stash == MASK_CODE:
145+
masked = True
27146

147+
checked_field_list = check_packed_fields(field_list)
28148

149+
if needmask and not masked:
150+
assert MASK_CODE in checked_field_list
29151

30-
def test_parse_arguments_invalid_vlist_format(monkeypatch):
152+
elif not needmask:
153+
assert field_list == checked_field_list
154+
155+
# Define a strategy to generate mock fields
156+
def field_strategy():
157+
return st.builds(
158+
lambda: MagicMock(copy=MagicMock(return_value=MagicMock())), # Each field has a copy method
159+
)
160+
161+
# Define a strategy to generate a list of mock fields
162+
field_list_strategy = st.lists(field_strategy(), min_size=1, max_size=10)
163+
164+
@given(
165+
filtered_fields=field_list_strategy
166+
)
167+
def test_append_fields_hypothesis(filtered_fields):
31168
"""
32-
Test parse_arguments with improperly formatted -v argument.
169+
Test the append_fields function with Hypothesis to ensure it works with various inputs.
33170
"""
34-
monkeypatch.setattr("sys.argv", ["script_name", "-i", "input.um", "-o", "output.um", "-v", "1,a,3"])
35-
with pytest.raises(ValueError):
36-
parse_arguments()
171+
# Mock the output file
172+
outfile = MagicMock()
173+
outfile.fields = []
174+
175+
# Call the function
176+
append_fields(outfile, filtered_fields)
177+
178+
# Assert the length of outfile.fields matches filtered_fields
179+
assert len(outfile.fields) == len(filtered_fields)
180+
181+
# Assert that each field in filtered_fields has its `copy` method called and is in outfile.fields
182+
for original_field, appended_field in zip(filtered_fields, outfile.fields):
183+
# Check that `copy` was called on the field
184+
original_field.copy.assert_called_once()
185+
# Check that the copied field matches what's in outfile.fields
186+
assert appended_field == original_field.copy.return_value
187+
188+
189+
def test_void_validation(capfd):
190+
"""Test that the void_validation function doesn't do anything but printing a message to stdout, for any input arguments."""
191+
args = [1, "test", None, False]
192+
kwargs = {"a": 1, "b": "test", "c": None, "d": False}
193+
init_args = deepcopy(args)
194+
init_kwargs = deepcopy(kwargs)
195+
result = void_validation(*args, **kwargs)
196+
# Capture the output
197+
captured = capfd.readouterr()
198+
# Test output message to stdout
199+
assert (
200+
captured.out.strip() == 'Skipping mule validation. To enable the validation, run using the "--validate" option.'
201+
)
202+
# Test no output message to stderr
203+
assert captured.err == ""
204+
# Test no return value
205+
assert result is None
206+
# Test no side effects for input arguments
207+
assert args == init_args
208+
assert kwargs == init_kwargs
209+
210+
212,0-1 Bot
211+
37212

0 commit comments

Comments
 (0)