|
1 | 1 | from pyspi.utils import filter_spis |
2 | 2 | import pytest |
3 | 3 | import yaml |
| 4 | +from unittest.mock import mock_open, patch |
| 5 | + |
| 6 | +@pytest.fixture |
| 7 | +def mock_yaml_content(): |
| 8 | + return { |
| 9 | + "module1": { |
| 10 | + "spi1": {"labels": ["keyword1", "keyword2"], "configs": [1, 2]}, |
| 11 | + "spi2": {"labels": ["keyword1"], "configs": [3]}, |
| 12 | + }, |
| 13 | + "module2": { |
| 14 | + "spi3": {"labels": ["keyword3"], "configs": [1, 2, 3]}, |
| 15 | + }, |
| 16 | + } |
4 | 17 |
|
5 | 18 | def test_filter_spis_invalid_keywords(): |
6 | 19 | """Pass in a dataype other than a list for the keywords""" |
7 | | - with pytest.raises(TypeError) as excinfo: |
8 | | - filter_spis("pyspi/config.yaml", "linear") |
9 | | - assert "Keywords must be passed as a list" in str(excinfo.value), "Keywords must be passed as list error not shown." |
| 20 | + with pytest.raises(ValueError) as excinfo: |
| 21 | + filter_spis(keywords="linear", configfile="pyspi/config.yaml") |
| 22 | + assert "Keywords must be provided as a list of strings" in str(excinfo.value) |
| 23 | + # check for passing in an empty list |
| 24 | + with pytest.raises(ValueError) as excinfo: |
| 25 | + filter_spis(keywords=[], configfile="pyspi/config.yaml") |
| 26 | + assert "At least one keyword must be provided" in str(excinfo.value) |
| 27 | + with pytest.raises(ValueError) as excinfo: |
| 28 | + filter_spis(keywords=[4], configfile="pyspi/config.yaml") |
| 29 | + assert "All keywords must be strings" in str(excinfo.value) |
10 | 30 |
|
11 | 31 | def test_filter_spis_with_invalid_config(): |
12 | 32 | """Pass in an invalid/missing config file""" |
13 | 33 | with pytest.raises(FileNotFoundError): |
14 | | - filter_spis("invalid_config.yaml", ["test"]) |
| 34 | + filter_spis(keywords=["test"], configfile="invalid_config.yaml") |
15 | 35 |
|
16 | | -def test_filter_spis_no_matches(): |
| 36 | +def test_filter_spis_no_matches(mock_yaml_content): |
17 | 37 | """Pass in keywords that return no spis and check for ValuError""" |
18 | | - mock_yaml_content = { |
19 | | - "module1": { |
20 | | - "spi1": {"labels": ["keyword1", "keyword2"], "configs": [1, 2]}, |
21 | | - "spi2": {"labels": ["keyword1"], "configs": [3]} |
22 | | - } |
23 | | - } |
| 38 | + m = mock_open() |
| 39 | + m().read.return_value = yaml.dump(mock_yaml_content) |
24 | 40 | keywords = ["random_keyword"] |
25 | 41 |
|
26 | | - # create temporary YAML to load into the function |
27 | | - with open("pyspi/mock_config2.yaml", "w") as f: |
28 | | - yaml.dump(mock_yaml_content, f) |
29 | | - |
30 | | - with pytest.raises(ValueError) as excinfo: |
31 | | - filter_spis("pyspi/mock_config2.yaml", keywords, name="mock_filtered_config") |
| 42 | + with patch("builtins.open", m), \ |
| 43 | + patch("os.path.isfile", return_value=True), \ |
| 44 | + patch("yaml.load", return_value=mock_yaml_content): |
| 45 | + with pytest.raises(ValueError) as excinfo: |
| 46 | + filter_spis(keywords=keywords, output_name="mock_filtered_config", configfile="./mock_config.yaml") |
| 47 | + |
32 | 48 | assert "0 SPIs were found" in str(excinfo.value), "Incorrect error message returned when no keywords match found." |
33 | 49 |
|
34 | | -def test_filter_spis_normal_operation(): |
| 50 | +def test_filter_spis_normal_operation(mock_yaml_content): |
35 | 51 | """Test whether the filter spis function works as expected""" |
36 | | - # create some mock content to filter |
37 | | - mock_yaml_content = { |
38 | | - "module1": { |
39 | | - "spi1": {"labels": ["keyword1", "keyword2"], "configs": [1, 2]}, |
40 | | - "spi2": {"labels": ["keyword1"], "configs": [3]} |
41 | | - } |
42 | | - } |
43 | | - keywords = ["keyword1", "keyword2"] |
| 52 | + m = mock_open() |
| 53 | + m().read_return_value = yaml.dump(mock_yaml_content) |
| 54 | + keywords = ["keyword1", "keyword2"] # filter keys |
44 | 55 | expected_output_yaml = { |
45 | 56 | "module1": { |
46 | 57 | "spi1": {"labels": ["keyword1", "keyword2"], "configs": [1,2]} |
47 | 58 | } |
48 | 59 | } |
49 | 60 |
|
50 | | - # create temporary YAML to load into the function |
51 | | - with open("pyspi/mock_config.yaml", "w") as f: |
52 | | - yaml.dump(mock_yaml_content, f) |
| 61 | + with patch("builtins.open", m), patch("os.path.isfile", return_value=True), \ |
| 62 | + patch("yaml.load", return_value=mock_yaml_content), \ |
| 63 | + patch("yaml.dump") as mock_dump: |
| 64 | + |
| 65 | + filter_spis(keywords=keywords, output_name="mock_filtered_config", configfile="./mock_config.yaml") |
| 66 | + |
| 67 | + mock_dump.assert_called_once() |
| 68 | + args, _ = mock_dump.call_args # get call args for dump and intercept |
| 69 | + actual_output = args[0] # the first argument to yaml.dump should be the yaml |
| 70 | + |
| 71 | + assert actual_output == expected_output_yaml, "Expected filtered YAML does not match actual filtered YAML." |
| 72 | + |
| 73 | +def test_filter_spis_io_error_on_read(): |
| 74 | + # check to see whether io error is raised when trying to access the configfile |
| 75 | + with patch("builtins.open", mock_open(read_data="data")) as mocked_file: |
| 76 | + mocked_file.side_effect = IOError("error") |
| 77 | + with pytest.raises(IOError): |
| 78 | + filter_spis(["keyword"], "output", "config.yaml") |
| 79 | + |
| 80 | +def test_filter_spis_saves_with_random_name_if_no_name_provided(mock_yaml_content): |
| 81 | + # mock os.urandom to return a predictable name |
| 82 | + random_bytes = bytes([1, 2, 3, 4]) |
| 83 | + expected_random_part = "01020304" |
| 84 | + |
| 85 | + with patch("builtins.open", mock_open()) as mocked_file, patch("os.path.isfile", return_value=True), \ |
| 86 | + patch("yaml.load", return_value=mock_yaml_content), patch("os.urandom", return_value=random_bytes): |
| 87 | + |
| 88 | + # run the filter function without providing an output name |
| 89 | + filter_spis(["keyword1"]) |
| 90 | + |
| 91 | + # construct the expected output name |
| 92 | + expected_file_name_pattern = f"config_{expected_random_part}.yaml" |
| 93 | + |
| 94 | + # check the mocked open function to see if file with expected name is opened (for writing) |
| 95 | + call_args_list = mocked_file.call_args_list |
| 96 | + found_expected_call = any( |
| 97 | + expected_file_name_pattern in call_args.args[0] and |
| 98 | + ('w' in call_args.args[1] if len(call_args.args) > 1 else 'w' in call_args.kwargs.get('mode', '')) |
| 99 | + for call_args in call_args_list |
| 100 | + ) |
| 101 | + |
| 102 | + assert found_expected_call, f"no file with the expected name {expected_file_name_pattern} was saved." |
| 103 | + |
| 104 | +def test_loads_default_config_if_no_config_specified(mock_yaml_content): |
| 105 | + script_dir = "/fake/script/directory" |
| 106 | + default_config_path = f"{script_dir}/config.yaml" |
53 | 107 |
|
54 | | - |
55 | | - filter_spis("pyspi/mock_config.yaml", keywords, name="mock_filtered_config") |
| 108 | + with patch("builtins.open", mock_open()) as mocked_open, \ |
| 109 | + patch("os.path.isfile", return_value=True), \ |
| 110 | + patch("yaml.load", return_value=mock_yaml_content), \ |
| 111 | + patch("os.path.dirname", return_value=script_dir), \ |
| 112 | + patch("os.path.abspath", return_value=script_dir): |
| 113 | + |
| 114 | + # run filter func without specifying a config file |
| 115 | + filter_spis(["keyword1"]) |
56 | 116 |
|
57 | | - # load in the output |
58 | | - with open("pyspi/mock_filtered_config.yaml", "r") as f: |
59 | | - actual_output = yaml.load(f, Loader=yaml.FullLoader) |
60 | | - |
61 | | - assert actual_output == expected_output_yaml, "Expected filtered YAML does not match actual filtered YAML." |
| 117 | + # ensure the mock_open was called with the expected path |
| 118 | + assert any(call.args[0] == default_config_path for call in mocked_open.mock_calls), \ |
| 119 | + "Expected default config file to be opened." |
0 commit comments