Skip to content

Commit 609aa55

Browse files
yileicopybara-github
authored andcommitted
absltest: do not fail tests on Python 3.12+ when no tests ran and:
- Either test filtering is used - Or sharding is used and the shard index > 0, i.e. only the first shard will fail when no tests ran on Python 3.12+. Context: Python 3.12 unittest will now fail when no tests ran after the change from python/cpython#102051. Since `absltest` is built on top of `unittest`, it will follow this behavior change in Python 3.12. However, when test filtering is used in `absltest`, often used via `bazel test --test_filter=<my_filter>`, the current user expectation is the `bazel test` command should NOT fail is at least one test ran. Since the test runner here has no visibility of the overall `bazel` invocation, we'll make the test not fail when test filtering is used via bazel's environment variable. This is the existing behavior before Python 3.12. Also test absl-py on Python 3.12. PiperOrigin-RevId: 565190992
1 parent f9281cb commit 609aa55

File tree

8 files changed

+191
-41
lines changed

8 files changed

+191
-41
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
strategy:
2020
fail-fast: false
2121
matrix:
22-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
22+
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
2323
os: [ubuntu-latest, macOS-latest, windows-latest]
2424

2525
steps:
@@ -30,6 +30,7 @@ jobs:
3030
id: setup_python
3131
with:
3232
python-version: ${{ matrix.python-version }}
33+
allow-prereleases: true
3334

3435
- name: Install virtualenv
3536
run: |

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com).
1010

1111
* `absl-py` no longer supports Python 3.6. It has reached end-of-life for more
1212
than a year now.
13+
* Support Python 3.12.
1314
* (logging) `logging.exception` can now take `exc_info` as argument, with
1415
default value `True`. Prior to this change setting `exc_info` would raise
1516
`KeyError`, this change fixes this behaviour.

absl/testing/BUILD

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,16 @@ py_test(
163163
name = "tests/absltest_sharding_test",
164164
size = "small",
165165
srcs = ["tests/absltest_sharding_test.py"],
166-
data = [":tests/absltest_sharding_test_helper"],
166+
data = [
167+
":tests/absltest_sharding_test_helper",
168+
":tests/absltest_sharding_test_helper_no_tests",
169+
],
167170
python_version = "PY3",
168171
srcs_version = "PY3",
169172
deps = [
170173
":_bazelize_command",
171174
":absltest",
175+
":parameterized",
172176
":tests/absltest_env",
173177
],
174178
)
@@ -182,6 +186,13 @@ py_binary(
182186
deps = [":absltest"],
183187
)
184188

189+
py_binary(
190+
name = "tests/absltest_sharding_test_helper_no_tests",
191+
testonly = 1,
192+
srcs = ["tests/absltest_sharding_test_helper_no_tests.py"],
193+
deps = [":absltest"],
194+
)
195+
185196
py_test(
186197
name = "tests/absltest_test",
187198
size = "small",

absl/testing/absltest.py

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,8 +2317,7 @@ def get_default_xml_output_filename():
23172317
os.path.splitext(os.path.basename(sys.argv[0]))[0] + '.xml')
23182318

23192319

2320-
def _setup_filtering(argv):
2321-
# type: (MutableSequence[Text]) -> None
2320+
def _setup_filtering(argv: MutableSequence[str]) -> bool:
23222321
"""Implements the bazel test filtering protocol.
23232322
23242323
The following environment variable is used in this method:
@@ -2333,16 +2332,20 @@ def _setup_filtering(argv):
23332332
23342333
Args:
23352334
argv: the argv to mutate in-place.
2335+
2336+
Returns:
2337+
Whether test filtering is requested.
23362338
"""
23372339
test_filter = os.environ.get('TESTBRIDGE_TEST_ONLY')
23382340
if argv is None or not test_filter:
2339-
return
2341+
return False
23402342

23412343
filters = shlex.split(test_filter)
23422344
if sys.version_info[:2] >= (3, 7):
23432345
filters = ['-k=' + test_filter for test_filter in filters]
23442346

23452347
argv[1:1] = filters
2348+
return True
23462349

23472350

23482351
def _setup_test_runner_fail_fast(argv):
@@ -2369,8 +2372,9 @@ def _setup_test_runner_fail_fast(argv):
23692372
argv[1:1] = ['--failfast']
23702373

23712374

2372-
def _setup_sharding(custom_loader=None):
2373-
# type: (Optional[unittest.TestLoader]) -> unittest.TestLoader
2375+
def _setup_sharding(
2376+
custom_loader: Optional[unittest.TestLoader] = None,
2377+
) -> Tuple[unittest.TestLoader, Optional[int]]:
23742378
"""Implements the bazel sharding protocol.
23752379
23762380
The following environment variables are used in this method:
@@ -2389,8 +2393,10 @@ def _setup_sharding(custom_loader=None):
23892393
custom_loader: A TestLoader to be made sharded.
23902394
23912395
Returns:
2392-
The test loader for shard-filtering or the standard test loader, depending
2393-
on the sharding environment variables.
2396+
A tuple of ``(test_loader, shard_index)``. ``test_loader`` is for
2397+
shard-filtering or the standard test loader depending on the sharding
2398+
environment variables. ``shard_index`` is the shard index, or ``None`` when
2399+
sharding is not used.
23942400
"""
23952401

23962402
# It may be useful to write the shard file even if the other sharding
@@ -2408,7 +2414,7 @@ def _setup_sharding(custom_loader=None):
24082414
base_loader = custom_loader or TestLoader()
24092415
if 'TEST_TOTAL_SHARDS' not in os.environ:
24102416
# Not using sharding, use the expected test loader.
2411-
return base_loader
2417+
return base_loader, None
24122418

24132419
total_shards = int(os.environ['TEST_TOTAL_SHARDS'])
24142420
shard_index = int(os.environ['TEST_SHARD_INDEX'])
@@ -2437,25 +2443,70 @@ def getShardedTestCaseNames(testCaseClass):
24372443
return [x for x in ordered_names if x in filtered_names]
24382444

24392445
base_loader.getTestCaseNames = getShardedTestCaseNames
2440-
return base_loader
2446+
return base_loader, shard_index
2447+
2448+
2449+
def _run_and_get_tests_result(
2450+
argv: MutableSequence[str],
2451+
args: Sequence[Any],
2452+
kwargs: MutableMapping[str, Any],
2453+
xml_test_runner_class: Type[unittest.TextTestRunner],
2454+
) -> Tuple[unittest.TestResult, bool]:
2455+
"""Same as run_tests, but it doesn't exit.
24412456
2457+
Args:
2458+
argv: sys.argv with the command-line flags removed from the front, i.e. the
2459+
argv with which :func:`app.run()<absl.app.run>` has called
2460+
``__main__.main``. It is passed to
2461+
``unittest.TestProgram.__init__(argv=)``, which does its own flag parsing.
2462+
It is ignored if kwargs contains an argv entry.
2463+
args: Positional arguments passed through to
2464+
``unittest.TestProgram.__init__``.
2465+
kwargs: Keyword arguments passed through to
2466+
``unittest.TestProgram.__init__``.
2467+
xml_test_runner_class: The type of the test runner class.
24422468
2443-
# pylint: disable=line-too-long
2444-
def _run_and_get_tests_result(argv, args, kwargs, xml_test_runner_class):
2445-
# type: (MutableSequence[Text], Sequence[Any], MutableMapping[Text, Any], Type) -> unittest.TestResult
2446-
# pylint: enable=line-too-long
2447-
"""Same as run_tests, except it returns the result instead of exiting."""
2469+
Returns:
2470+
A tuple of ``(test_result, fail_when_no_tests_ran)``.
2471+
``fail_when_no_tests_ran`` indicates whether the test should fail when
2472+
no tests ran.
2473+
"""
24482474

24492475
# The entry from kwargs overrides argv.
24502476
argv = kwargs.pop('argv', argv)
24512477

2478+
if sys.version_info[:2] >= (3, 12):
2479+
# Python 3.12 unittest changed the behavior from PASS to FAIL in
2480+
# https://github.com/python/cpython/pull/102051. absltest follows this.
2481+
fail_when_no_tests_ran = True
2482+
else:
2483+
# Historically, absltest and unittest before Python 3.12 passes if no tests
2484+
# ran.
2485+
fail_when_no_tests_ran = False
2486+
24522487
# Set up test filtering if requested in environment.
2453-
_setup_filtering(argv)
2488+
if _setup_filtering(argv):
2489+
# When test filtering is requested, ideally we also want to fail when no
2490+
# tests ran. However, the test filters are usually done when running bazel.
2491+
# When you run multiple targets, e.g. `bazel test //my_dir/...
2492+
# --test_filter=MyTest`, you don't necessarily want individual tests to fail
2493+
# because no tests match in that particular target.
2494+
# Due to this use case, we don't fail when test filtering is requested via
2495+
# the environment variable from bazel.
2496+
fail_when_no_tests_ran = False
2497+
24542498
# Set up --failfast as requested in environment
24552499
_setup_test_runner_fail_fast(argv)
24562500

24572501
# Shard the (default or custom) loader if sharding is turned on.
2458-
kwargs['testLoader'] = _setup_sharding(kwargs.get('testLoader', None))
2502+
kwargs['testLoader'], shard_index = _setup_sharding(
2503+
kwargs.get('testLoader', None)
2504+
)
2505+
if shard_index is not None and shard_index > 0:
2506+
# When sharding is requested, all the shards except the first one shall not
2507+
# fail when no tests ran. This happens when the shard count is greater than
2508+
# the test case count.
2509+
fail_when_no_tests_ran = False
24592510

24602511
# XML file name is based upon (sorted by priority):
24612512
# --xml_output_file flag, XML_OUTPUT_FILE variable,
@@ -2533,9 +2584,13 @@ def _run_and_get_tests_result(argv, args, kwargs, xml_test_runner_class):
25332584
# on argv, which is sys.argv without the command-line flags.
25342585
kwargs['argv'] = argv
25352586

2587+
# Request unittest.TestProgram to not exit. The exit will be handled by
2588+
# `absltest.run_tests`.
2589+
kwargs['exit'] = False
2590+
25362591
try:
25372592
test_program = unittest.TestProgram(*args, **kwargs)
2538-
return test_program.result
2593+
return test_program.result, fail_when_no_tests_ran
25392594
finally:
25402595
if xml_buffer:
25412596
try:
@@ -2545,9 +2600,11 @@ def _run_and_get_tests_result(argv, args, kwargs, xml_test_runner_class):
25452600
xml_buffer.close()
25462601

25472602

2548-
def run_tests(argv, args, kwargs): # pylint: disable=line-too-long
2549-
# type: (MutableSequence[Text], Sequence[Any], MutableMapping[Text, Any]) -> None
2550-
# pylint: enable=line-too-long
2603+
def run_tests(
2604+
argv: MutableSequence[Text],
2605+
args: Sequence[Any],
2606+
kwargs: MutableMapping[Text, Any],
2607+
) -> None:
25512608
"""Executes a set of Python unit tests.
25522609
25532610
Most users should call absltest.main() instead of run_tests.
@@ -2568,8 +2625,13 @@ def run_tests(argv, args, kwargs): # pylint: disable=line-too-long
25682625
kwargs: Keyword arguments passed through to
25692626
``unittest.TestProgram.__init__``.
25702627
"""
2571-
result = _run_and_get_tests_result(
2572-
argv, args, kwargs, xml_reporter.TextAndXMLTestRunner)
2628+
result, fail_when_no_tests_ran = _run_and_get_tests_result(
2629+
argv, args, kwargs, xml_reporter.TextAndXMLTestRunner
2630+
)
2631+
if fail_when_no_tests_ran and result.testsRun == 0:
2632+
# Python 3.12 unittest exits with 5 when no tests ran. The code comes from
2633+
# pytest which does the same thing.
2634+
sys.exit(5)
25732635
sys.exit(not result.wasSuccessful())
25742636

25752637

absl/testing/tests/absltest_filtering_test.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ def test_not_found_filters_py36(self, use_env_variable, use_app_run):
156156
def test_not_found_filters_py37(self, use_env_variable, use_app_run):
157157
out, exit_code = self._run_filtered('NotExistedClass.not_existed_method',
158158
use_env_variable, use_app_run)
159-
self.assertEqual(0, exit_code)
159+
if not use_env_variable and sys.version_info[:2] >= (3, 12):
160+
# When test filter is requested with the unittest `-k` flag, absltest
161+
# respect unittest to fail when no tests run on Python 3.12+.
162+
self.assertEqual(5, exit_code)
163+
else:
164+
self.assertEqual(0, exit_code)
160165
self.assertIn('Ran 0 tests', out)
161166

162167
@absltest.skipIf(

absl/testing/tests/absltest_sharding_test.py

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,50 @@
1616

1717
import os
1818
import subprocess
19+
import sys
1920

2021
from absl.testing import _bazelize_command
2122
from absl.testing import absltest
23+
from absl.testing import parameterized
2224
from absl.testing.tests import absltest_env
2325

2426

2527
NUM_TEST_METHODS = 8 # Hard-coded, based on absltest_sharding_test_helper.py
2628

2729

28-
class TestShardingTest(absltest.TestCase):
30+
class TestShardingTest(parameterized.TestCase):
2931
"""Integration tests: Runs a test binary with sharding.
3032
3133
This is done by setting the sharding environment variables.
3234
"""
3335

3436
def setUp(self):
3537
super().setUp()
36-
self._test_name = 'absl/testing/tests/absltest_sharding_test_helper'
3738
self._shard_file = None
3839

3940
def tearDown(self):
4041
super().tearDown()
4142
if self._shard_file is not None and os.path.exists(self._shard_file):
4243
os.unlink(self._shard_file)
4344

44-
def _run_sharded(self,
45-
total_shards,
46-
shard_index,
47-
shard_file=None,
48-
additional_env=None):
45+
def _run_sharded(
46+
self,
47+
total_shards,
48+
shard_index,
49+
shard_file=None,
50+
additional_env=None,
51+
helper_name='absltest_sharding_test_helper',
52+
):
4953
"""Runs the py_test binary in a subprocess.
5054
5155
Args:
5256
total_shards: int, the total number of shards.
5357
shard_index: int, the shard index.
54-
shard_file: string, if not 'None', the path to the shard file.
55-
This method asserts it is properly created.
58+
shard_file: string, if not 'None', the path to the shard file. This method
59+
asserts it is properly created.
5660
additional_env: Additional environment variables to be set for the py_test
5761
binary.
62+
helper_name: The name of the helper binary.
5863
5964
Returns:
6065
(stdout, exit_code) tuple of (string, int).
@@ -72,12 +77,14 @@ def _run_sharded(self,
7277
if os.path.exists(shard_file):
7378
os.unlink(shard_file)
7479

80+
helper = 'absl/testing/tests/' + helper_name
7581
proc = subprocess.Popen(
76-
args=[_bazelize_command.get_executable_path(self._test_name)],
82+
args=[_bazelize_command.get_executable_path(helper)],
7783
env=env,
7884
stdout=subprocess.PIPE,
7985
stderr=subprocess.STDOUT,
80-
universal_newlines=True)
86+
universal_newlines=True,
87+
)
8188
stdout = proc.communicate()[0]
8289

8390
if shard_file:
@@ -140,7 +147,12 @@ def test_with_one_shard(self):
140147
self._assert_sharding_correctness(1)
141148

142149
def test_with_ten_shards(self):
143-
self._assert_sharding_correctness(10)
150+
shards = 10
151+
# This test relies on the shard count to be greater than the number of
152+
# tests, to ensure that the non-zero shards won't fail even if no tests ran
153+
# on Python 3.12+.
154+
self.assertGreater(shards, NUM_TEST_METHODS)
155+
self._assert_sharding_correctness(shards)
144156

145157
def test_sharding_with_randomization(self):
146158
# If we're both sharding *and* randomizing, we need to confirm that we
@@ -156,6 +168,32 @@ def test_sharding_with_randomization(self):
156168
self.assertEqual(set(first_tests), set(second_tests))
157169
self.assertNotEqual(first_tests, second_tests)
158170

171+
@parameterized.named_parameters(
172+
('total_1_index_0', 1, 0, None),
173+
('total_2_index_0', 2, 0, None),
174+
# The 2nd shard (index=1) should not fail.
175+
('total_2_index_1', 2, 1, 0),
176+
)
177+
def test_no_tests_ran(
178+
self, total_shards, shard_index, override_expected_exit_code
179+
):
180+
if override_expected_exit_code is not None:
181+
expected_exit_code = override_expected_exit_code
182+
elif sys.version_info[:2] >= (3, 12):
183+
expected_exit_code = 5
184+
else:
185+
expected_exit_code = 0
186+
out, exit_code = self._run_sharded(
187+
total_shards,
188+
shard_index,
189+
helper_name='absltest_sharding_test_helper_no_tests',
190+
)
191+
self.assertEqual(
192+
expected_exit_code,
193+
exit_code,
194+
'Unexpected exit code, output:\n{}'.format(out),
195+
)
196+
159197

160198
if __name__ == '__main__':
161199
absltest.main()

0 commit comments

Comments
 (0)