diff --git a/django/test/runner.py b/django/test/runner.py index c19e622170..65a399a709 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -852,6 +852,10 @@ def partition_suite_by_case(suite): def test_match_tags(test, tags, exclude_tags): + if isinstance(test, unittest.loader._FailedTest): + # Tests that couldn't load always match to prevent tests from falsely + # passing due e.g. to syntax errors. + return True test_tags = set(getattr(test, 'tags', [])) test_fn_name = getattr(test, '_testMethodName', str(test)) if hasattr(test, test_fn_name): diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 40ca4b0334..f942518353 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1497,6 +1497,12 @@ don't. Runs only tests :ref:`marked with the specified tags `. May be specified multiple times and combined with :option:`test --exclude-tag`. +Tests that fail to load are always considered matching. + +.. versionchanged:: 4.0 + + In older versions, tests that failed to load did not match tags. + .. option:: --exclude-tag EXCLUDE_TAGS Excludes tests :ref:`marked with the specified tags `. diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index d84e029ccd..c7a9bb87b9 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -373,6 +373,9 @@ Miscellaneous non-boolean attribute without a value equal to an attribute with the same name and value. +* Tests that fail to load, for example due to syntax errors, now always match + when using :option:`test --tag`. + .. _deprecated-features-4.0: Features deprecated in 4.0 diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index 366d180aa5..6b19f2f274 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/tests/test_runner/test_discover_runner.py @@ -2,6 +2,7 @@ import os import unittest.loader from argparse import ArgumentParser from contextlib import contextmanager +from importlib import import_module from unittest import TestSuite, TextTestRunner, defaultTestLoader, mock from django.db import connections @@ -259,6 +260,18 @@ class DiscoverRunnerTests(SimpleTestCase): self.assertEqual(count_tests(tags=['foo'], exclude_tags=['bar', 'baz']), 1) self.assertEqual(count_tests(exclude_tags=['foo']), 0) + def test_tag_fail_to_load(self): + with self.assertRaises(SyntaxError): + import_module('test_runner_apps.tagged.tests_syntax_error') + runner = DiscoverRunner(tags=['syntax_error']) + # A label that doesn't exist or cannot be loaded due to syntax errors + # is always considered matching. + suite = runner.build_suite(['doesnotexist', 'test_runner_apps.tagged']) + self.assertEqual([test.id() for test in suite], [ + 'unittest.loader._FailedTest.doesnotexist', + 'unittest.loader._FailedTest.test_runner_apps.tagged.tests_syntax_error', + ]) + def test_included_tags_displayed(self): runner = DiscoverRunner(tags=['foo', 'bar'], verbosity=2) with captured_stdout() as stdout: diff --git a/tests/test_runner_apps/tagged/tests_syntax_error.py b/tests/test_runner_apps/tagged/tests_syntax_error.py new file mode 100644 index 0000000000..2714768a30 --- /dev/null +++ b/tests/test_runner_apps/tagged/tests_syntax_error.py @@ -0,0 +1,11 @@ +from unittest import TestCase + +from django.test import tag + + +@tag('syntax_error') +class SyntaxErrorTestCase(TestCase): + pass + + +1syntax_error # NOQA