blob: b2e2eb68c78f0d363960f110999cd01faa059442 [file] [log] [blame]
John Caterc8932862020-01-31 10:55:01 -08001# Copyright 2020 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# Lint as: python3
16"""Tests for unittest.bash."""
17
18from __future__ import absolute_import
19from __future__ import division
20from __future__ import print_function
21
22import os
23import re
24import shutil
25import stat
26import subprocess
27import tempfile
ajurkowski71c66782022-01-07 10:11:40 -080028import textwrap
John Caterc8932862020-01-31 10:55:01 -080029import unittest
30
31# The test setup for this external test is forwarded to the internal bash test.
32# This allows the internal test to use the same runfiles to load unittest.bash.
33_TEST_PREAMBLE = """
34#!/bin/bash
35# --- begin runfiles.bash initialization ---
36if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
37 source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
38else
39 echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
40 exit 1
41fi
42# --- end runfiles.bash initialization ---
43
44echo "Writing XML to ${XML_OUTPUT_FILE}"
45
46source "$(rlocation "io_bazel/src/test/shell/unittest.bash")" \
47 || { echo "Could not source unittest.bash" >&2; exit 1; }
48"""
49
50ANSI_ESCAPE = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]")
51
52
53def remove_ansi(line):
54 """Remove ANSI-style escape sequences from the input."""
55 return ANSI_ESCAPE.sub("", line)
56
57
58class TestResult(object):
59 """Save test results for easy checking."""
60
61 def __init__(self, asserter, return_code, output, xmlfile):
62 self._asserter = asserter
63 self._return_code = return_code
64 self._output = remove_ansi(output)
65
66 # Read in the XML result file.
67 if os.path.isfile(xmlfile):
68 with open(xmlfile, "r") as f:
69 self._xml = f.read()
70 else:
71 # Unable to read the file, errors will be reported later.
72 self._xml = ""
73
74 # Methods to assert on the state of the results.
75
76 def assertLogMessage(self, message):
77 self._asserter.assertRegex(self._output, message)
78
jcater2d40d5c2020-01-31 12:01:30 -080079 def assertNotLogMessage(self, message):
80 self._asserter.assertNotRegex(self._output, message)
81
John Caterc8932862020-01-31 10:55:01 -080082 def assertXmlMessage(self, message):
83 self._asserter.assertRegex(self._xml, message)
84
85 def assertSuccess(self, suite_name):
86 self._asserter.assertEqual(0, self._return_code,
87 "Script failed unexpectedly:\n%s" % self._output)
88 self.assertLogMessage(suite_name)
89 self.assertXmlMessage("failures=\"0\"")
90 self.assertXmlMessage("errors=\"0\"")
91
92 def assertNotSuccess(self, suite_name, failures=0, errors=0):
93 self._asserter.assertNotEqual(0, self._return_code)
94 self.assertLogMessage(suite_name)
95 if failures:
96 self.assertXmlMessage("failures=\"%d\"" % failures)
97 if errors:
98 self.assertXmlMessage("errors=\"%d\"" % errors)
99
100 def assertTestPassed(self, test_name):
101 self.assertLogMessage("PASSED: %s" % test_name)
102
103 def assertTestFailed(self, test_name, message=""):
104 self.assertLogMessage("%s FAILED" % test_name)
105 if message:
106 self.assertLogMessage("FAILED: %s" % message)
107
108
109class UnittestTest(unittest.TestCase):
110
111 def setUp(self):
112 """Create a working directory under our temp dir."""
113 super(UnittestTest, self).setUp()
114 self.work_dir = tempfile.mkdtemp(dir=os.environ["TEST_TMPDIR"])
115
116 def tearDown(self):
117 """Clean up the working directory."""
118 super(UnittestTest, self).tearDown()
119 shutil.rmtree(self.work_dir)
120
121 def write_file(self, filename, contents=""):
122 """Write the contents to a file in the workdir."""
123
124 filepath = os.path.join(self.work_dir, filename)
125 with open(filepath, "w") as f:
126 f.write(_TEST_PREAMBLE.strip())
127 f.write(contents)
128 os.chmod(filepath, stat.S_IEXEC | stat.S_IWRITE | stat.S_IREAD)
129
130 def find_runfiles(self):
131 if "RUNFILES_DIR" in os.environ:
132 return os.environ["RUNFILES_DIR"]
133
134 # Fall back to being based on the srcdir.
135 if "TEST_SRCDIR" in os.environ:
136 return os.environ["TEST_SRCDIR"]
137
138 # Base on the current dir
139 return "%s/.." % os.getcwd()
140
ajurkowski71c66782022-01-07 10:11:40 -0800141 def execute_test(self, filename, env=None, args=()):
John Caterc8932862020-01-31 10:55:01 -0800142 """Executes the file and stores the results."""
143
144 filepath = os.path.join(self.work_dir, filename)
145 xmlfile = os.path.join(self.work_dir, "dummy-testlog.xml")
jcater2d40d5c2020-01-31 12:01:30 -0800146 test_env = {
John Caterc8932862020-01-31 10:55:01 -0800147 "TEST_TMPDIR": self.work_dir,
148 "RUNFILES_DIR": self.find_runfiles(),
149 "TEST_SRCDIR": os.environ["TEST_SRCDIR"],
150 "XML_OUTPUT_FILE": xmlfile,
151 }
jcater2d40d5c2020-01-31 12:01:30 -0800152 # Add in env, forcing everything to be a string.
153 if env:
154 for k, v in env.items():
155 test_env[k] = str(v)
John Caterc8932862020-01-31 10:55:01 -0800156 completed = subprocess.run(
ajurkowski71c66782022-01-07 10:11:40 -0800157 [filepath, *args],
jcater2d40d5c2020-01-31 12:01:30 -0800158 env=test_env,
John Caterc8932862020-01-31 10:55:01 -0800159 stdout=subprocess.PIPE,
160 stderr=subprocess.STDOUT,
161 )
162 return TestResult(self, completed.returncode,
163 completed.stdout.decode("utf-8"), xmlfile)
164
165 # Actual test cases.
166
167 def test_success(self):
168 self.write_file(
169 "thing.sh", """
170function test_success() {
171 echo foo >&${TEST_log} || fail "expected echo to succeed"
172 expect_log "foo"
173}
174
175run_suite "success tests"
176""")
177
178 result = self.execute_test("thing.sh")
179 result.assertSuccess("success tests")
180 result.assertTestPassed("test_success")
181
182 def test_timestamp(self):
183 self.write_file(
184 "thing.sh", """
185function test_timestamp() {
186 local ts=$(timestamp)
187 [[ $ts =~ ^[0-9]{13}$ ]] || fail "timestamp wan't valid: $ts"
188
189 local time_diff=$(get_run_time 100000 223456)
190 assert_equals $time_diff 123.456
191}
192
193run_suite "timestamp tests"
194""")
195
196 result = self.execute_test("thing.sh")
197 result.assertSuccess("timestamp tests")
198 result.assertTestPassed("test_timestamp")
199
200 def test_failure(self):
201 self.write_file(
202 "thing.sh", """
203function test_failure() {
204 fail "I'm a failure with <>&\\" escaped symbols"
205}
206
207run_suite "failure tests"
208""")
209
210 result = self.execute_test("thing.sh")
211 result.assertNotSuccess("failure tests", failures=0, errors=1)
212 result.assertTestFailed("test_failure")
213 result.assertXmlMessage(
214 "message=\"I'm a failure with &lt;&gt;&amp;&quot; escaped symbols\"")
215 result.assertXmlMessage("I'm a failure with <>&\" escaped symbols")
216
ajurkowski39f95f42021-10-06 12:02:20 -0700217 def test_set_bash_errexit_prints_stack_trace(self):
218 self.write_file(
219 "thing.sh", """
220set -euo pipefail
221
222function helper() {
223 echo before
224 false
225 echo after
226}
227
228function test_failure_in_helper() {
229 helper
230}
231
232run_suite "bash errexit tests"
233""")
234
235 result = self.execute_test("thing.sh")
236 result.assertNotSuccess("bash errexit tests")
237 result.assertTestFailed("test_failure_in_helper")
238 result.assertLogMessage(r"./thing.sh:\d*: in call to helper")
239 result.assertLogMessage(
240 r"./thing.sh:\d*: in call to test_failure_in_helper")
241
242 def test_set_bash_errexit_runs_tear_down(self):
243 self.write_file(
244 "thing.sh", """
245set -euo pipefail
246
247function tear_down() {
248 echo "Running tear_down"
249}
250
251function testenv_tear_down() {
252 echo "Running testenv_tear_down"
253}
254
255function test_failure_in_helper() {
256 wrong_command
257}
258
259run_suite "bash errexit tests"
260""")
261
262 result = self.execute_test("thing.sh")
263 result.assertNotSuccess("bash errexit tests")
264 result.assertTestFailed("test_failure_in_helper")
265 result.assertLogMessage("Running tear_down")
266 result.assertLogMessage("Running testenv_tear_down")
267
ajurkowskif67395b2021-10-13 10:44:03 -0700268 def test_set_bash_errexit_pipefail_propagates_failure_through_pipe(self):
269 self.write_file(
270 "thing.sh", """
271set -euo pipefail
272
273function test_pipefail() {
274 wrong_command | cat
275 echo after
276}
277
278run_suite "bash errexit tests"
279""")
280
281 result = self.execute_test("thing.sh")
282 result.assertNotSuccess("bash errexit tests")
283 result.assertTestFailed("test_pipefail")
284 result.assertLogMessage("wrong_command: command not found")
285 result.assertNotLogMessage("after")
286
287 def test_set_bash_errexit_no_pipefail_ignores_failure_before_pipe(self):
288 self.write_file(
289 "thing.sh", """
290set -eu
291set +o pipefail
292
293function test_nopipefail() {
294 wrong_command | cat
295 echo after
296}
297
298run_suite "bash errexit tests"
299""")
300
301 result = self.execute_test("thing.sh")
302 result.assertSuccess("bash errexit tests")
303 result.assertTestPassed("test_nopipefail")
304 result.assertLogMessage("wrong_command: command not found")
305 result.assertLogMessage("after")
306
ajurkowskif40e4012021-10-07 09:09:27 -0700307 def test_set_bash_errexit_pipefail_long_testname_succeeds(self):
308 test_name = "x" * 1000
309 self.write_file(
310 "thing.sh", """
311set -euo pipefail
312
313function test_%s() {
314 :
315}
316
317run_suite "bash errexit tests"
318""" % test_name)
319
320 result = self.execute_test("thing.sh")
321 result.assertSuccess("bash errexit tests")
322
jcater2d40d5c2020-01-31 12:01:30 -0800323 def test_empty_test_fails(self):
324 self.write_file("thing.sh", """
325# No tests present.
326
327run_suite "empty test suite"
328""")
329
330 result = self.execute_test("thing.sh")
331 result.assertNotSuccess("empty test suite")
332 result.assertLogMessage("No tests found.")
333
334 def test_empty_test_succeeds_sharding(self):
335 self.write_file(
336 "thing.sh", """
337# Only one test.
338function test_thing() {
339 echo
340}
341
342run_suite "empty test suite"
343""")
344
345 # First shard.
346 result = self.execute_test(
347 "thing.sh", env={
348 "TEST_TOTAL_SHARDS": 2,
349 "TEST_SHARD_INDEX": 0,
350 })
351 result.assertSuccess("empty test suite")
352 result.assertLogMessage("No tests executed due to sharding")
353
354 # Second shard.
355 result = self.execute_test(
356 "thing.sh", env={
357 "TEST_TOTAL_SHARDS": 2,
358 "TEST_SHARD_INDEX": 1,
359 })
360 result.assertSuccess("empty test suite")
361 result.assertNotLogMessage("No tests")
362
ajurkowski71c66782022-01-07 10:11:40 -0800363 def test_filter_runs_only_matching_test(self):
364 self.write_file(
365 "thing.sh",
366 textwrap.dedent("""
367 function test_abc() {
368 :
369 }
370
371 function test_def() {
372 echo "running def"
373 }
374
375 run_suite "tests to filter"
376 """))
377
378 result = self.execute_test(
379 "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_a*"})
380
381 result.assertSuccess("tests to filter")
382 result.assertTestPassed("test_abc")
383 result.assertNotLogMessage("running def")
384
385 def test_filter_prefix_match_only_skips_test(self):
386 self.write_file(
387 "thing.sh",
388 textwrap.dedent("""
389 function test_abc() {
390 echo "running abc"
391 }
392
393 run_suite "tests to filter"
394 """))
395
396 result = self.execute_test(
397 "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_a"})
398
399 result.assertNotSuccess("tests to filter")
400 result.assertLogMessage("No tests found.")
401
402 def test_filter_multiple_globs_runs_tests_matching_any(self):
403 self.write_file(
404 "thing.sh",
405 textwrap.dedent("""
406 function test_abc() {
407 echo "running abc"
408 }
409
410 function test_def() {
411 echo "running def"
412 }
413
414 run_suite "tests to filter"
415 """))
416
417 result = self.execute_test(
418 "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "donotmatch:*a*"})
419
420 result.assertSuccess("tests to filter")
421 result.assertTestPassed("test_abc")
422 result.assertNotLogMessage("running def")
423
424 def test_filter_character_group_runs_only_matching_tests(self):
425 self.write_file(
426 "thing.sh",
427 textwrap.dedent("""
428 function test_aaa() {
429 :
430 }
431
432 function test_daa() {
433 :
434 }
435
436 function test_zaa() {
437 echo "running zaa"
438 }
439
440 run_suite "tests to filter"
441 """))
442
443 result = self.execute_test(
444 "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_[a-f]aa"})
445
446 result.assertSuccess("tests to filter")
447 result.assertTestPassed("test_aaa")
448 result.assertTestPassed("test_daa")
449 result.assertNotLogMessage("running zaa")
450
451 def test_filter_sharded_runs_subset_of_filtered_tests(self):
452 for index in range(2):
453 with self.subTest(index=index):
454 self.__filter_sharded_runs_subset_of_filtered_tests(index)
455
456 def __filter_sharded_runs_subset_of_filtered_tests(self, index):
457 self.write_file(
458 "thing.sh",
459 textwrap.dedent("""
460 function test_a0() {
461 echo "running a0"
462 }
463
464 function test_a1() {
465 echo "running a1"
466 }
467
468 function test_bb() {
469 echo "running bb"
470 }
471
472 run_suite "tests to filter"
473 """))
474
475 result = self.execute_test(
476 "thing.sh",
477 env={
478 "TESTBRIDGE_TEST_ONLY": "test_a*",
479 "TEST_TOTAL_SHARDS": 2,
480 "TEST_SHARD_INDEX": index
481 })
482
483 result.assertSuccess("tests to filter")
484 # The sharding logic is shifted by 1, starts with 2nd shard.
485 result.assertTestPassed("test_a" + str(index ^ 1))
486 result.assertLogMessage("running a" + str(index ^ 1))
487 result.assertNotLogMessage("running a" + str(index))
488 result.assertNotLogMessage("running bb")
489
490 def test_arg_runs_only_matching_test_and_issues_warning(self):
491 self.write_file(
492 "thing.sh",
493 textwrap.dedent("""
494 function test_abc() {
495 :
496 }
497
498 function test_def() {
499 echo "running def"
500 }
501
502 run_suite "tests to filter"
503 """))
504
505 result = self.execute_test("thing.sh", args=["test_abc"])
506
507 result.assertSuccess("tests to filter")
508 result.assertTestPassed("test_abc")
509 result.assertNotLogMessage("running def")
510 result.assertLogMessage(
511 r"WARNING: Passing test names in arguments \(--test_arg\) is "
512 "deprecated, please use --test_filter='test_abc' instead.")
513
514 def test_arg_multiple_tests_issues_warning_with_test_filter_command(self):
515 self.write_file(
516 "thing.sh",
517 textwrap.dedent("""
518 function test_abc() {
519 :
520 }
521
522 function test_def() {
523 :
524 }
525
526 run_suite "tests to filter"
527 """))
528
529 result = self.execute_test("thing.sh", args=["test_abc", "test_def"])
530
531 result.assertSuccess("tests to filter")
532 result.assertTestPassed("test_abc")
533 result.assertTestPassed("test_def")
534 result.assertLogMessage(
535 r"WARNING: Passing test names in arguments \(--test_arg\) is "
536 "deprecated, please use --test_filter='test_abc:test_def' instead.")
537
538 def test_arg_and_filter_ignores_arg(self):
539 self.write_file(
540 "thing.sh",
541 textwrap.dedent("""
542 function test_abc() {
543 :
544 }
545
546 function test_def() {
547 echo "running def"
548 }
549
550 run_suite "tests to filter"
551 """))
552
553 result = self.execute_test(
554 "thing.sh", args=["test_def"], env={"TESTBRIDGE_TEST_ONLY": "test_a*"})
555
556 result.assertSuccess("tests to filter")
557 result.assertTestPassed("test_abc")
558 result.assertNotLogMessage("running def")
559 result.assertLogMessage(
560 "WARNING: Both --test_arg and --test_filter specified, ignoring --test_arg"
561 )
562
ajurkowski4e1c6852022-01-07 13:16:26 -0800563 def test_custom_ifs_variable_finds_and_runs_test(self):
564 for sharded in (False, True):
565 with self.subTest(sharded=sharded):
566 self.__custom_ifs_variable_finds_and_runs_test(sharded)
567
568 def __custom_ifs_variable_finds_and_runs_test(self, sharded):
569 self.write_file(
570 "thing.sh",
571 textwrap.dedent(r"""
572 IFS=$'\t'
573 function test_foo() {
574 :
575 }
576
577 run_suite "custom IFS test"
578 """))
579
580 result = self.execute_test(
581 "thing.sh",
582 env={} if not sharded else {
583 "TEST_TOTAL_SHARDS": 2,
584 "TEST_SHARD_INDEX": 1
585 })
586 result.assertSuccess("custom IFS test")
587 result.assertTestPassed("test_foo")
588
John Caterc8932862020-01-31 10:55:01 -0800589
590if __name__ == "__main__":
591 unittest.main()