|
1 | 1 | 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 |
3 | 7 |
|
| 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] |
4 | 18 |
|
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", |
7 | 38 | [
|
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 | + ], |
15 | 56 | )
|
| 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 |
16 | 112 |
|
| 113 | + # Call the function being tested |
| 114 | + filtered_fields = filter_fields(input_file, prognostic, include_list, exclude_list) |
17 | 115 |
|
| 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 |
18 | 128 |
|
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): |
20 | 132 | """
|
21 |
| - Test parse_arguments function with valid inputs. |
| 133 | + Test the check_packed_fields function with different scenarios. |
22 | 134 | """
|
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 |
27 | 146 |
|
| 147 | + checked_field_list = check_packed_fields(field_list) |
28 | 148 |
|
| 149 | + if needmask and not masked: |
| 150 | + assert MASK_CODE in checked_field_list |
29 | 151 |
|
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): |
31 | 168 | """
|
32 |
| - Test parse_arguments with improperly formatted -v argument. |
| 169 | + Test the append_fields function with Hypothesis to ensure it works with various inputs. |
33 | 170 | """
|
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 | + |
37 | 212 |
|
0 commit comments