Skip to content

Move to nose2 only#6146

Open
klecki wants to merge 11 commits intoNVIDIA:mainfrom
klecki:nose2-only
Open

Move to nose2 only#6146
klecki wants to merge 11 commits intoNVIDIA:mainfrom
klecki:nose2-only

Conversation

@klecki
Copy link
Contributor

@klecki klecki commented Dec 22, 2025

Use nose2 as the only testing framework. Drop nose.

Category: Other

Description:

Remove the WARs used to keep nose alive.

nose2 supports the yield-style test discovery by default @attr has a different filtering syntax (-A) and just checks for presence of truthy test_foo.attribute_name. A decorator uses this mechanism for backward compatibility.

nose2 splits with_setup(setup, teardown) into two separate decorators, a backward compatible decorator is added.

nottest sets special attribute.

SkipTest from unittest is recommended to be used directly (with the same functionality).

Test scripts are adjusted with minimal changes to run through nose2. Followup cleanup can be used for renaming.

Replace unsupported -m regex by attributes

Additional information:

Affected modules and functionalities:

Key points relevant for the review:

Tests:

  • Existing tests apply
  • New tests added
    • Python tests
    • GTests
    • Benchmark
    • Other
  • N/A

Checklist

Documentation

  • Existing documentation applies
  • Documentation updated
    • Docstring
    • Doxygen
    • RST
    • Jupyter
    • Other
  • N/A

DALI team only

Requirements

  • Implements new requirements
  • Affects existing requirements
  • N/A

REQ IDs: N/A

JIRA TASK: N/A

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Dec 22, 2025

Greptile Summary

This PR completes the migration from nose to nose2 as the sole Python test runner for DALI. It removes all the nose-compatibility shims from nose_utils.py (imp module mocking, pkg_resources patching, collections.abc workarounds), deletes the nose_wrapper package, and rewrites all generator-based yield tests as class-based parameterized tests using nose2.tools.params / cartesian_params. A new custom plugin (nose2_attrib_generators) is introduced to gate generator-function execution behind attribute filters, preserving the pre-invocation import-guard behaviour of the old yield style.

Key observations:

  • The nose_utils.py rewrite cleanly replaces nose.tools.assert_raises/assert_warns with unittest.TestCase equivalents; the callable and context-manager forms are both handled correctly.
  • All CI shell scripts correctly replace --attr with -A and drop the .py extension from module names.
  • MXNet support is removed across tests and CI scripts as expected.
  • test_dali_tf_exec2.py introduces a session-based execution fallback that uses the private tf.compat.v1._eager_context attribute to detect cross-test eager-mode pollution; a public-API check (tf.is_tensor) would be more stable.
  • test_dali_tf_dataset_eager.py's test_tf_dataset_with_stop_iter is not parameterized (unlike its graph counterpart), reducing failure-diagnosis granularity.
  • nose2_attrib_generators._patch_generator_plugin relies on the undocumented return-value contract of nose2's private _testsFromGeneratorFunc; the contract should be documented or guarded against future breakage.

Confidence Score: 4/5

  • This PR is safe to merge; it is a well-structured test-infrastructure migration with no production-code changes.
  • The changes are confined to the test infrastructure and CI scripts. The core migration is correct: all --attr flags are converted to -A, .py extensions are stripped, and generator tests are properly converted to class-based parameterized tests. The main concerns are: (1) a private TF API used in test_dali_tf_exec2 that could break silently on a TF update, (2) an assumed-but-undocumented return-value contract for the monkey-patched nose2 plugin method, and (3) one non-parameterized loop test in the eager TF dataset suite. None of these are showstoppers, but they reduce the score slightly below 5.
  • dali/test/python/test_dali_tf_exec2.py (private TF API access) and dali/test/python/nose2_attrib_generators.py (private nose2 method contract).

Important Files Changed

Filename Overview
dali/test/python/nose2_attrib_generators.py New nose2 plugin that monkey-patches the Generators plugin to filter generator functions by attributes before calling them, preventing unnecessary imports. Uses private class-name string matching to find plugins, and replicates nose2's attribute-parsing logic in _build_attribs_list, which could silently diverge from nose2's internals if the framework changes its -A parsing.
dali/test/python/nose_utils.py Major simplification: drops all the nose-compatibility shims (imp module, pkg_resources, collections.abc patches) and reimplements attr, nottest, assert_raises, and assert_warns on top of unittest directly. The assert_raises/assert_warns callable-form path no longer returns a value, which is consistent with unittest semantics.
dali/test/python/test_dali_tf_exec2.py Converted to class-based test using unittest.TestCase. Added unnecessary and fragile session-based execution fallback that accesses the private TF API attribute tf.compat.v1._eager_context. The original test had a simple assert; the new path is needed to handle cross-test eager-execution pollution but uses private internals.
dali/test/python/operator_1/test_numba_func.py Converted all generator-based tests into class-based parameterized tests using nose2's @params decorator. setUp calls check_numba_compatibility_cpu/gpu. All test methods retain their @attr("sanitizer_skip") decorators.
qa/test_template_impl.sh Removes the nose_wrapper and python_invoke_test infrastructure entirely; now only python_new_invoke_test (nose2) remains. Drops nose and nose-timer from pip_packages. Clean removal.
dali/test/python/unittest.cfg Adds nose2_attrib_generators as the first plugin across all three cfg files (unittest.cfg, unittest_failure.cfg, unittest_slow.cfg), ensuring the custom generator-attribute-filter plugin is active for all test runs.
dali/test/python/test_dali_tf_dataset_eager.py Generator-based tests converted to class-based with setUp calling skip_inputs_for_incompatible_tf. test_tf_dataset_with_stop_iter was collapsed into a single non-parameterized loop method (unlike the graph counterpart which uses @cartesian_params), reducing per-combination failure visibility.
dali/test/python/test_external_source_parallel.py Large refactor: generator-based tests ported to class-based tests with setUp/tearDown lifecycle. Fork-only tests correctly made private (_test_parallel_fork_cpu_only) and called explicitly from CI scripts. Imports moved to test_pool_utils appropriately.
qa/nose_wrapper/main.py Deleted as part of the nose removal. The wrapper contained Python 3.11 inspect.getargspec compatibility shim and nose.core.run_exit. No longer needed.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["python -m nose2\n(python_new_invoke_test)"] --> B["nose2 loads plugins from unittest.cfg"]
    B --> C["nose2.plugins.attrib\n(AttributeSelector)"]
    B --> D["nose2_attrib_generators\n(AttributeGeneratorFilter)\nalwaysOn=True"]
    B --> E["nose2.plugins.collect\nnose2.plugins.printhooks"]

    D --> F["handleArgs()\n→ _patch_generator_plugin()"]
    F --> G["Find Generators plugin\nin session.plugins"]
    G --> H["Monkey-patch\nGenerators._testsFromGeneratorFunc"]

    H --> I{"-A attribute filter?"}
    I -- "No filter" --> J["Call original\n_testsFromGeneratorFunc"]
    I -- "Filter present" --> K["_matches_attrib_filter(obj)\nchecks func attributes"]
    K -- "Match" --> J
    K -- "No match" --> L["return []  ← skip generator,\nno import/execution"]

    J --> M["Generator function called\n→ yields test cases\n(via nose2 params/cartesian_params)"]

    C --> N["Per-test method:\nAttributeSelector filters\nby -A expression"]
    N --> O["Run test method\n(with setUp/tearDown)"]
Loading

Comments Outside Diff (2)

  1. dali/test/python/test_dali_tf_exec2.py, line 2595-2612 (link)

    Private TF internal API usage in added session fallback

    Lines 2597 (getattr(tf.compat.v1, "_eager_context", None) is not None) access an undocumented private TF attribute. The original test body simply did:

    pos, neg = tf_function_with_conditionals(dali_dataset.take(5))
    assert pos == 3
    assert neg == 2

    The new session-based fallback was added to cope with cross-test eager-execution pollution (e.g. test_dali_tf_dataset_mnist_graph.py calls tf.compat.v1.disable_eager_execution() at module level). However, _eager_context is an internal attribute that may not exist or may change meaning across TF versions.

    A more robust approach would be to check and restore eager mode in setUp/tearDown:

    def setUp(self):
        skip_inputs_for_incompatible_tf()
        self._was_eager = tf.executing_eagerly()
        if not self._was_eager:
            tf.compat.v1.enable_eager_execution()

    Or simply check tf.is_tensor(pos) (which is a public API) to decide whether .numpy() is needed.

  2. dali/test/python/nose2_attrib_generators.py, line 101-137 (link)

    Monkey-patch fragility: _testsFromGeneratorFunc return-value contract assumed

    patched_tests_from_gen returns [] when the generator should be filtered out. This is correct only if the Generators plugin's internal caller uses the return value to populate the test suite. If nose2 ever changes _testsFromGeneratorFunc to write directly into event (returning None) rather than returning a list, the return [] early-exit would silently stop filtering generator functions — the generator would never be called, but the caller would also not receive an empty list.

    Since _testsFromGeneratorFunc is a private method (_ prefix), it is not part of nose2's public API and is free to change without notice. A defensive guard would help catch regressions:

    result = original_tests_from_gen(event, obj)
    # Sanity-check that the contract still holds
    if result is not None and not isinstance(result, (list, tuple)):
        log.warning("_testsFromGeneratorFunc return type changed; attribute filter may not work")

    At minimum, a comment explaining the assumed contract would make future maintenance safer.

Last reviewed commit: a45d7b9

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (1)

  1. dali/test/python/nose_utils.py, line 129-130 (link)

    style: creating a new TestCase instance for each assertion call is unconventional

    Consider creating a module-level instance once:

    Then define tc at module level (after imports) and reuse it in both assert_raises and assert_warns

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

25 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (2)

  1. dali/test/python/test_functional_api.py, line 22 (link)

    logic: This file still imports nose directly and uses nose.tools.eq_ on line 188. Since nose is being removed as a dependency, this will cause an import error.

  2. dali/test/python/test_functional_api.py, line 188 (link)

    logic: Replace nose.tools.eq_ with standard assertion or use assert_equals from nose_utils

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

28 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (1)

  1. dali/test/python/nose_utils.py, line 68 (link)

    syntax: missing self parameter - instance methods must have self as first parameter

29 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@klecki
Copy link
Contributor Author

klecki commented Dec 22, 2025

!build

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [40627092]: BUILD STARTED

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [40627092]: BUILD FAILED

@klecki
Copy link
Contributor Author

klecki commented Dec 22, 2025

!build

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [40628704]: BUILD STARTED

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (1)

  1. dali/test/python/nose_utils.py, line 83-85 (link)

    style: Consider using the module-level _test_case instance instead of creating a new empty_case() for each assertion:

    This is more efficient and aligns with how assert_raises and assert_warns are implemented.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

29 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@JanuszL JanuszL self-assigned this Dec 22, 2025
@JanuszL
Copy link
Contributor

JanuszL commented Dec 22, 2025

Do we need to touch unittest_failure.cfg, unittest_slow.cfg or unittest.cfg as well?

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [40628704]: BUILD FAILED

Copy link
Contributor

@mzient mzient left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the tests are first discovered and then run will most certainly prevent parallel external source from working (see test job 247483858) was the primary reason for not pursuing full transition to nose2 to date.
Perhaps you can move the troublesome tests to separate files to guarantee proper execution order.

Also, some of the tests used to generate vast amounts of data in the "discovery" stage, because yielding in nose didn't cause data accumulation. Please make sure that this is not the case any more - the tests might work when run one by one but case out-of-memory condition when multiple test files are run.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (1)

  1. dali/test/python/nose_utils.py, line 74 (link)

    syntax: Missing self parameter in method definition

33 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Dec 30, 2025

Greptile's behavior is changing!

From now on, if a review finishes with no comments, we will not post an additional "statistics" comment to confirm that our review found nothing to comment on. However, you can confirm that we reviewed your changes in the status check section.

This feature can be toggled off in your Code Review Settings by deselecting "Create a status check for each PR".

Signed-off-by: Krzysztof Lecki <klecki@nvidia.com>
@JanuszL JanuszL marked this pull request as ready for review March 9, 2026 21:11
Comment on lines 45 to +48
pass


# Module-level TestCase instance for assertions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_setup no longer exported — silent breakage risk

The old nose_utils.py exported with_setup via from nose import SkipTest, with_setup. The new file no longer provides or exports with_setup. All call-sites in the PR have been migrated, but any out-of-tree test file or future code that still does from nose_utils import with_setup will get an ImportError at import time.

If a backward-compatible shim is desirable (the PR description mentions "a backward compatible decorator is added"), adding even a simple one would reduce breakage risk:

def with_setup(setup=None, teardown=None):
    """Backward-compatible shim – prefer class-level setUp/tearDown instead."""
    import warnings, functools
    warnings.warn("with_setup is deprecated; use setUp/tearDown in a test class", DeprecationWarning)
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if setup:
                setup()
            try:
                return func(*args, **kwargs)
            finally:
                if teardown:
                    teardown()
        return wrapper
    return decorator

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point is to move away from nose2 for good without providing backward compatibility.

@JanuszL
Copy link
Contributor

JanuszL commented Mar 9, 2026

!build

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45742567]: BUILD STARTED

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45742567]: BUILD FAILED


test_pytorch() {
${python_invoke_test} --attr '!slow,pytorch' test_dali_variable_batch_size.py
${python_new_invoke_test} --attr '!slow,pytorch' test_dali_variable_batch_size.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line was not fully converted from the old nose invocation style to nose2. It has two problems:

  1. --attr is not a nose2 flag — nose2 uses -A for attribute filtering. With nose2, --attr is an unknown argument and will likely either error out or silently skip the attribute filtering entirely, causing all tests (including slow/non-pytorch ones) to run without filtering.

  2. .py extension in the module path — nose2 takes Python module names (dot-separated), not file names. All other migrated calls in this script already use ${test_script%.py} or bare module names (e.g., line 33).

Suggested change
${python_new_invoke_test} --attr '!slow,pytorch' test_dali_variable_batch_size.py
${python_new_invoke_test} -A '!slow,pytorch' test_dali_variable_batch_size

Signed-off-by: Janusz Lisiecki <jlisiecki@nvidia.com>
@JanuszL
Copy link
Contributor

JanuszL commented Mar 9, 2026

!build

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45743750]: BUILD STARTED

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45743750]: BUILD FAILED

expected_out,
)

def test_numba_func_with_cond(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @attr("sanitizer_skip") decorator

test_numba_func_with_cond is missing the @attr("sanitizer_skip") decorator that was present on the original standalone function. Without this attribute, the test will not be excluded when running with sanitizers enabled (via the -A '!sanitizer_skip' filter used in CI), causing potential test failures or false positives in sanitizer runs.

Suggested change
def test_numba_func_with_cond(self):
@attr("sanitizer_skip")
def test_numba_func_with_cond(self):

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

expected_out=[np.full((10, 10, 10), 255, dtype=np.uint8)],
enable_conditionals=True,
)
def test_numba_func_with_cond_do_not_convert(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @attr("sanitizer_skip") decorator

test_numba_func_with_cond_do_not_convert is missing the @attr("sanitizer_skip") decorator that was present on the original standalone function. Without this attribute, the test will not be excluded when running with sanitizers enabled (via the -A '!sanitizer_skip' filter used in CI), causing potential test failures or false positives in sanitizer runs.

Suggested change
def test_numba_func_with_cond_do_not_convert(self):
@attr("sanitizer_skip")
def test_numba_func_with_cond_do_not_convert(self):

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Signed-off-by: Janusz Lisiecki <jlisiecki@nvidia.com>
Comment on lines +46 to +82
def _build_attribs_list(self, attrib_plugin):
"""Build the attribs list from the attrib plugin's -A configuration.

This replicates the logic from AttributeSelector.moduleLoadedSuite
for -A filters only (not -E eval filters).
"""
attribs = []

# Handle -A (attribute) filters
for attr in attrib_plugin.attribs:
attr_group = []
for attrib in attr.strip().split(","):
if not attrib:
continue
items = attrib.split("=", 1)
if len(items) > 1:
# "name=value"
key, value = items
else:
key = items[0]
if key[0] == "!":
# "!name"
key = key[1:]
value = False
else:
# "name"
value = True
attr_group.append((key, value))
attribs.append(attr_group)

return attribs

def _matches_attrib_filter(self, test_func, attrib_plugin):
"""Check if test_func matches the attribute filter from attrib plugin."""
if not attrib_plugin:
return True

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated internal attribute-parsing logic from nose2

_build_attribs_list manually replicates the parsing logic inside AttributeSelector.moduleLoadedSuite (including the comma-split, =-split, and !-negation handling). If nose2 changes how it parses -A expressions (e.g. to support quoting, ranges, or OR-groups within a single -A value), this copy will silently diverge and produce incorrect filter decisions for generator functions only.

Consider whether you can obtain the parsed attribs structure directly from the attrib_plugin instance after argument processing, rather than re-parsing from the raw string. For example, attrib_plugin.attribs may already be processed by the time handleArgs fires. If not, at minimum adding a comment explaining that this must be kept in sync with nose2's AttributeSelector internals would prevent silent future breakage.

@JanuszL
Copy link
Contributor

JanuszL commented Mar 10, 2026

!build

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45822574]: BUILD STARTED

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45743750]: BUILD PASSED

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45822574]: BUILD FAILED

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45822574]: BUILD PASSED

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: there could only two classes, one for CPU and one for GPU tests and they'd all contain multiple test functions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Signed-off-by: Janusz Lisiecki <jlisiecki@nvidia.com>
@JanuszL
Copy link
Contributor

JanuszL commented Mar 12, 2026

!build

run_fn,
out_types,
in_types,
outs_ndim,

Check notice

Code scanning / CodeQL

Unnecessary lambda Note test

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.
[3],
rot_image_setup,
None,
lambda x: np.rot90(x),

Check notice

Code scanning / CodeQL

Unnecessary lambda Note test

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.
@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45987009]: BUILD STARTED

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45987009]: BUILD FAILED

@dali-automaton
Copy link
Collaborator

CI MESSAGE: [45987009]: BUILD PASSED

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants