File: validation_unit_test.py

package info (click to toggle)
python-sigima 1.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 24,956 kB
  • sloc: python: 33,326; makefile: 3
file content (357 lines) | stat: -rw-r--r-- 14,616 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Testing validation test introspection and CSV generation.
"""

import inspect
import os.path as osp
import re
import pytest

import sigima.tests as tests_pkg
from sigima.proc.decorator import find_computation_functions
from sigima.proc.validation import (
    ValidationStatistics,
    generate_valid_test_names_for_function,
    get_validation_tests,
)
from sigima.tests.helpers import WorkdirRestoringTempDir


def __generate_all_valid_test_names() -> set[str]:
    """Generate all valid test names for all computation functions.

    Returns:
        Set of all valid test names that could test computation functions
    """
    computation_functions = find_computation_functions()
    valid_test_names = set()

    for module_name, func_name, _ in computation_functions:
        names = generate_valid_test_names_for_function(module_name, func_name)
        valid_test_names.update(names)

    return valid_test_names


def test_validation_statistics() -> None:
    """Test validation statistics introspection and CSV generation."""
    stats = ValidationStatistics()
    stats.collect_validation_status(verbose=True)
    stats.get_validation_info()
    with WorkdirRestoringTempDir() as tmpdir:
        stats.generate_csv_files(tmpdir)
        stats.generate_statistics_csv(tmpdir)


def test_validation_missing_tests() -> None:
    """Test that all computation functions have validation tests.

    This test ensures that all computation functions (those decorated with
    @computation_function) have corresponding validation tests
    marked with @pytest.mark.validation.
    """
    # Get all functions marked with @pytest.mark.validation
    validation_tests = get_validation_tests(tests_pkg)

    # Get all computation functions that should have validation tests
    computation_functions = find_computation_functions()
    required_functions = [
        (module_name, func_name) for module_name, func_name, _ in computation_functions
    ]

    # Check each required function to see if it has a corresponding validation test
    missing_validation_tests = []

    for module_name, func_name in required_functions:
        valid_test_names = generate_valid_test_names_for_function(
            module_name, func_name
        )

        # Check if any of the valid test names exist in validation tests
        has_validation_test = any(
            test_name in [vt[0] for vt in validation_tests]
            for test_name in valid_test_names
        )

        if not has_validation_test:
            missing_validation_tests.append(f"{module_name}.{func_name}")

    # Report any missing validation tests
    if missing_validation_tests:
        error_messages = []
        error_messages.append(
            "The following computation functions are missing "
            "validation tests marked with @pytest.mark.validation:"
        )
        for func_name in missing_validation_tests:
            error_messages.append(f"  - {func_name}")
        error_messages.append("")
        error_messages.append(f"Found {len(missing_validation_tests)} missing cases.")
        error_messages.append(
            "Please add validation tests for these computation functions."
        )

        raise AssertionError("\n".join(error_messages))


def test_validation_decorator_only_on_computation_functions() -> None:
    """Test that @pytest.mark.validation is only used on computation function tests.

    This test ensures that validation tests marked with @pytest.mark.validation
    are only used for testing actual computation functions (those decorated with
    @computation_function). Test functions for non-computation functions (like
    I/O convenience functions) should not have this decorator.
    """
    # Get all functions marked with @pytest.mark.validation
    validation_tests = get_validation_tests(tests_pkg)

    # Get all valid test names for computation functions
    valid_test_names = __generate_all_valid_test_names()

    # Check each validation test to see if it corresponds to a computation function
    invalid_validation_tests = []

    for test_name, test_path, line_number in validation_tests:
        if test_name not in valid_test_names:
            # This validation test doesn't correspond to any computation function
            rel_path = osp.relpath(test_path, start=osp.dirname(tests_pkg.__file__))
            module_parts = rel_path.replace(osp.sep, ".").replace(".py", "")
            module_name = f"sigima.tests.{module_parts}"
            invalid_validation_tests.append((test_name, module_name, line_number))

    # Report any invalid validation tests
    if invalid_validation_tests:
        error_messages = []
        error_messages.append(
            "Found @pytest.mark.validation decorator on tests that don't test "
            "computation functions:"
        )
        for test_name, module_name, line_number in invalid_validation_tests:
            # Convert module path back to file path for clickable links
            file_path = (
                module_name.replace("sigima.tests.", "").replace(".", "\\") + ".py"
            )
            error_messages.append(f"  - {file_path}:{line_number} ({test_name})")
        error_messages.append("")
        error_messages.append(f"Found {len(invalid_validation_tests)} invalid cases.")
        error_messages.append(
            "The @pytest.mark.validation decorator should only be used on "
            "test functions that test computation functions (those decorated with "
            "@computation_function). Please remove this decorator from test functions "
            "that test non-computation functions."
        )

        raise AssertionError("\n".join(error_messages))


@pytest.mark.skip(reason="Doc not installed")
def test_computation_functions_documented_in_features() -> None:
    """Test that all computation functions are documented in doc/features.rst.

    This test ensures that all computation functions (those decorated with
    @computation_function) are documented in the features.rst file with
    proper Sphinx :func: references using simplified paths like
    sigima.proc.image.function_name or sigima.proc.signal.function_name.
    """
    # Read the features.rst file
    doc_dir = osp.join(osp.dirname(tests_pkg.__file__), "..", "..", "doc")
    features_rst_path = osp.join(doc_dir, "user_guide", "features.rst")

    if not osp.exists(features_rst_path):
        raise AssertionError(f"Documentation file not found: {features_rst_path}")

    with open(features_rst_path, encoding="utf-8") as f:
        features_content = f.read()

    # Get all computation functions
    computation_functions = find_computation_functions()

    # Check each computation function to see if it's documented
    missing_documentation = []

    for module_name, func_name, _ in computation_functions:
        # Build the expected documentation reference using simplified path
        # The module_name is like "sigima.proc.image" or "sigima.proc.signal"
        module_path = module_name.split("sigima.proc.")[-1]
        expected_ref = f"sigima.proc.{module_path}.{func_name}"

        # Check if this reference exists in the documentation
        if expected_ref not in features_content:
            missing_documentation.append((module_name, func_name, expected_ref))

    # Report any missing documentation
    if missing_documentation:
        error_messages = []
        error_messages.append(
            "The following computation functions are missing from doc/features.rst:"
        )
        for module_name, func_name, expected_ref in missing_documentation:
            error_messages.append(f"  - {func_name} ({expected_ref})")
        error_messages.append("")
        error_messages.append(f"Found {len(missing_documentation)} missing cases.")
        error_messages.append(
            "Please add documentation references for these computation functions "
            "in doc/features.rst using the format:"
        )
        error_messages.append(
            "   :func:`function_name <sigima.proc.module.function_name>`"
        )

        raise AssertionError("\n".join(error_messages))


def _check_function_referenced_in_code(
    source_code: str, module_name: str, func_name: str
) -> bool:
    """Check if a computation function is referenced in source code.

    This is a simple check that looks for the fully qualified function name
    anywhere in the code (e.g., "sigima.proc.image.fliph").

    Args:
        source_code: Source code to check
        module_name: Module name (e.g., "sigima.proc.image")
        func_name: Function name (e.g., "fliph")

    Returns:
        True if the function is referenced in the code
    """
    # Simply check if the full qualified name appears in the source
    full_name = f"{module_name}.{func_name}"
    return full_name in source_code


def _get_helper_functions_called(source_code: str) -> set[str]:
    """Extract names of helper functions called in source code.

    Args:
        source_code: Source code to analyze

    Returns:
        Set of function names that are called
    """
    # Find all function calls that start with __ or _ (helper functions)
    pattern = r"\b(__\w+|_\w+)\s*\("
    matches = re.findall(pattern, source_code)
    return set(matches)


def test_validation_tests_call_computation_functions() -> None:
    """Test that validation tests actually call the computation functions they test.

    This test ensures that each validation test marked with @pytest.mark.validation
    references the corresponding computation function (those decorated with
    @computation_function) either directly or through helper functions in the
    same test module.
    """
    # Get all functions marked with @pytest.mark.validation
    validation_tests = get_validation_tests(tests_pkg)

    # Get all computation functions
    computation_functions = find_computation_functions()

    # Build a mapping from test names to computation functions
    test_to_function_map = {}
    for module_name, func_name, _ in computation_functions:
        valid_test_names = generate_valid_test_names_for_function(
            module_name, func_name
        )
        for test_name in valid_test_names:
            if test_name not in test_to_function_map:
                test_to_function_map[test_name] = []
            test_to_function_map[test_name].append((module_name, func_name))

    # Check each validation test to ensure it references the computation function
    tests_not_calling_function = []

    # pylint: disable=too-many-nested-blocks
    for test_name, test_path, line_number in validation_tests:
        if test_name not in test_to_function_map:
            # This test doesn't correspond to any computation function
            # (will be caught by another test)
            continue

        # Get the source code of the test function
        # Import the test module and get the function
        rel_path = osp.relpath(test_path, start=osp.dirname(tests_pkg.__file__))
        module_parts = rel_path.replace(osp.sep, ".").replace(".py", "")
        test_module_name = f"sigima.tests.{module_parts}"

        try:
            test_module = __import__(test_module_name, fromlist=[test_name])
            test_func = getattr(test_module, test_name)
            test_source_code = inspect.getsource(test_func)
        except (ImportError, AttributeError, OSError):
            # Skip if we can't get the source (e.g., built-in functions)
            continue

        # Check if the computation function is referenced in the test
        for module_name, func_name in test_to_function_map[test_name]:
            # Check if it's referenced directly in the test
            function_referenced = _check_function_referenced_in_code(
                test_source_code, module_name, func_name
            )

            # If not found directly, check helper functions
            if not function_referenced:
                helper_funcs = _get_helper_functions_called(test_source_code)

                for helper_name in helper_funcs:
                    try:
                        helper_func = getattr(test_module, helper_name, None)
                        if helper_func and callable(helper_func):
                            helper_source = inspect.getsource(helper_func)
                            if _check_function_referenced_in_code(
                                helper_source, module_name, func_name
                            ):
                                function_referenced = True
                                break
                    except (AttributeError, OSError):
                        # Can't get source for this helper, skip it
                        continue

            if not function_referenced:
                tests_not_calling_function.append(
                    (test_name, module_name, func_name, test_path, line_number)
                )

    # Report any validation tests that don't call their computation functions
    if tests_not_calling_function:
        error_messages = []
        error_messages.append(
            "The following validation tests don't call their corresponding "
            "computation functions:"
        )
        for (
            test_name,
            module_name,
            func_name,
            test_path,
            line_number,
        ) in tests_not_calling_function:
            rel_path = osp.relpath(test_path, start=osp.dirname(tests_pkg.__file__))
            file_path = rel_path.replace(osp.sep, "\\")
            error_messages.append(
                f"  - {file_path}:{line_number} ({test_name})\n"
                f"    Expected to call: {module_name}.{func_name}"
            )
        error_messages.append("")
        error_messages.append(f"Found {len(tests_not_calling_function)} invalid cases.")
        error_messages.append(
            "Validation tests must call the computation function they are testing, "
            "not the underlying utility functions. Please update these tests to call "
            "the computation function (e.g., sigima.proc.image.func_name(...)) "
            "instead of calling lower-level utility functions directly."
        )

        raise AssertionError("\n".join(error_messages))


if __name__ == "__main__":
    test_validation_statistics()
    test_validation_missing_tests()
    test_validation_decorator_only_on_computation_functions()
    test_computation_functions_documented_in_features()
    test_validation_tests_call_computation_functions()