1# 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"""Tests for unittest.bash.""" 16 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import print_function 20 21import os 22import re 23import shutil 24import stat 25import subprocess 26import tempfile 27import textwrap 28import unittest 29 30# The test setup for this external test is forwarded to the internal bash test. 31# This allows the internal test to use the same runfiles to load unittest.bash. 32_TEST_PREAMBLE = """ 33#!/bin/bash 34# --- begin runfiles.bash initialization --- 35if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then 36 source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash" 37else 38 echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash" 39 exit 1 40fi 41# --- end runfiles.bash initialization --- 42 43echo "Writing XML to ${XML_OUTPUT_FILE}" 44 45source "$(rlocation "rules_android/test/bashunit/unittest.bash")" \ 46 || { echo "Could not source unittest.bash" >&2; exit 1; } 47""" 48 49ANSI_ESCAPE = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]") 50 51 52def remove_ansi(line): 53 """Remove ANSI-style escape sequences from the input.""" 54 return ANSI_ESCAPE.sub("", line) 55 56 57class TestResult(object): 58 """Save test results for easy checking.""" 59 60 def __init__(self, asserter, return_code, output, xmlfile): 61 self._asserter = asserter 62 self._return_code = return_code 63 self._output = remove_ansi(output) 64 65 # Read in the XML result file. 66 if os.path.isfile(xmlfile): 67 with open(xmlfile, "r") as f: 68 self._xml = f.read() 69 else: 70 # Unable to read the file, errors will be reported later. 71 self._xml = "" 72 73 # Methods to assert on the state of the results. 74 75 def assertLogMessage(self, message): 76 self.assertExactlyOneMatch(self._output, message) 77 78 def assertNotLogMessage(self, message): 79 self._asserter.assertNotRegex(self._output, message) 80 81 def assertXmlMessage(self, message): 82 self.assertExactlyOneMatch(self._xml, message) 83 84 def assertNotXmlMessage(self, message): 85 self._asserter.assertNotRegex(self._xml, message) 86 87 def assertSuccess(self, suite_name): 88 self._asserter.assertEqual(0, self._return_code, 89 f"Script failed unexpectedly:\n{self._output}") 90 self.assertLogMessage(suite_name) 91 self.assertXmlMessage("<testsuites [^/]*failures=\"0\"") 92 self.assertXmlMessage("<testsuites [^/]*errors=\"0\"") 93 94 def assertNotSuccess(self, suite_name, failures=0, errors=0): 95 self._asserter.assertNotEqual(0, self._return_code) 96 self.assertLogMessage(suite_name) 97 if failures: 98 self.assertXmlMessage(f'<testsuites [^/]*failures="{failures}"') 99 if errors: 100 self.assertXmlMessage(f'<testsuites [^/]*errors="{errors}"') 101 102 def assertTestPassed(self, test_name): 103 self.assertLogMessage(f"PASSED: {test_name}") 104 105 def assertTestFailed(self, test_name, message=""): 106 self.assertLogMessage(f"{test_name} FAILED: {message}") 107 108 def assertExactlyOneMatch(self, text, pattern): 109 self._asserter.assertRegex(text, pattern) 110 self._asserter.assertEqual( 111 len(re.findall(pattern, text)), 112 1, 113 msg=f"Found more than 1 match of '{pattern}' in '{text}'") 114 115 116class UnittestTest(unittest.TestCase): 117 118 def setUp(self): 119 """Create a working directory under our temp dir.""" 120 super(UnittestTest, self).setUp() 121 self.work_dir = tempfile.mkdtemp(dir=os.environ["TEST_TMPDIR"]) 122 123 def tearDown(self): 124 """Clean up the working directory.""" 125 super(UnittestTest, self).tearDown() 126 shutil.rmtree(self.work_dir) 127 128 def write_file(self, filename, contents=""): 129 """Write the contents to a file in the workdir.""" 130 131 filepath = os.path.join(self.work_dir, filename) 132 with open(filepath, "w") as f: 133 f.write(_TEST_PREAMBLE.strip()) 134 f.write(contents) 135 os.chmod(filepath, stat.S_IEXEC | stat.S_IWRITE | stat.S_IREAD) 136 137 def find_runfiles(self): 138 if "RUNFILES_DIR" in os.environ: 139 return os.environ["RUNFILES_DIR"] 140 141 # Fall back to being based on the srcdir. 142 if "TEST_SRCDIR" in os.environ: 143 return os.environ["TEST_SRCDIR"] 144 145 # Base on the current dir 146 return f"{os.getcwd()}/.." 147 148 def execute_test(self, filename, env=None, args=()): 149 """Executes the file and stores the results.""" 150 151 filepath = os.path.join(self.work_dir, filename) 152 xmlfile = os.path.join(self.work_dir, "dummy-testlog.xml") 153 test_env = { 154 "TEST_TMPDIR": self.work_dir, 155 "RUNFILES_DIR": self.find_runfiles(), 156 "TEST_SRCDIR": os.environ["TEST_SRCDIR"], 157 "XML_OUTPUT_FILE": xmlfile, 158 } 159 # Add in env, forcing everything to be a string. 160 if env: 161 for k, v in env.items(): 162 test_env[k] = str(v) 163 completed = subprocess.run( 164 [filepath, *args], 165 env=test_env, 166 stdout=subprocess.PIPE, 167 stderr=subprocess.STDOUT, 168 ) 169 return TestResult(self, completed.returncode, 170 completed.stdout.decode("utf-8"), xmlfile) 171 172 # Actual test cases. 173 174 def test_success(self): 175 self.write_file( 176 "thing.sh", """ 177function test_success() { 178 echo foo >&${TEST_log} || fail "expected echo to succeed" 179 expect_log "foo" 180} 181 182run_suite "success tests" 183""") 184 185 result = self.execute_test("thing.sh") 186 result.assertSuccess("success tests") 187 result.assertTestPassed("test_success") 188 189 def test_timestamp(self): 190 self.write_file( 191 "thing.sh", """ 192function test_timestamp() { 193 local ts=$(timestamp) 194 [[ $ts =~ ^[0-9]{13}$ ]] || fail "timestamp wan't valid: $ts" 195 196 local time_diff=$(get_run_time 100000 223456) 197 assert_equals $time_diff 123.456 198} 199 200run_suite "timestamp tests" 201""") 202 203 result = self.execute_test("thing.sh") 204 result.assertSuccess("timestamp tests") 205 result.assertTestPassed("test_timestamp") 206 207 def test_failure(self): 208 self.write_file( 209 "thing.sh", """ 210function test_failure() { 211 fail "I'm a failure with <>&\\" escaped symbols" 212} 213 214run_suite "failure tests" 215""") 216 217 result = self.execute_test("thing.sh") 218 result.assertNotSuccess("failure tests", failures=0, errors=1) 219 result.assertTestFailed("test_failure") 220 result.assertXmlMessage( 221 "message=\"I'm a failure with <>&" escaped symbols\"") 222 result.assertXmlMessage("I'm a failure with <>&\" escaped symbols") 223 224 def test_set_bash_errexit_prints_stack_trace(self): 225 self.write_file( 226 "thing.sh", """ 227set -euo pipefail 228 229function helper() { 230 echo before 231 false 232 echo after 233} 234 235function test_failure_in_helper() { 236 helper 237} 238 239run_suite "bash errexit tests" 240""") 241 242 result = self.execute_test("thing.sh") 243 result.assertNotSuccess("bash errexit tests") 244 result.assertTestFailed("test_failure_in_helper") 245 result.assertLogMessage(r"./thing.sh:\d*: in call to helper") 246 result.assertLogMessage( 247 r"./thing.sh:\d*: in call to test_failure_in_helper") 248 249 def test_set_bash_errexit_runs_tear_down(self): 250 self.write_file( 251 "thing.sh", """ 252set -euo pipefail 253 254function tear_down() { 255 echo "Running tear_down" 256} 257 258function testenv_tear_down() { 259 echo "Running testenv_tear_down" 260} 261 262function test_failure_in_helper() { 263 wrong_command 264} 265 266run_suite "bash errexit tests" 267""") 268 269 result = self.execute_test("thing.sh") 270 result.assertNotSuccess("bash errexit tests") 271 result.assertTestFailed("test_failure_in_helper") 272 result.assertLogMessage("Running tear_down") 273 result.assertLogMessage("Running testenv_tear_down") 274 275 def test_set_bash_errexit_pipefail_propagates_failure_through_pipe(self): 276 self.write_file( 277 "thing.sh", """ 278set -euo pipefail 279 280function test_pipefail() { 281 wrong_command | cat 282 echo after 283} 284 285run_suite "bash errexit tests" 286""") 287 288 result = self.execute_test("thing.sh") 289 result.assertNotSuccess("bash errexit tests") 290 result.assertTestFailed("test_pipefail") 291 result.assertLogMessage("wrong_command: command not found") 292 result.assertNotLogMessage("after") 293 294 def test_set_bash_errexit_no_pipefail_ignores_failure_before_pipe(self): 295 self.write_file( 296 "thing.sh", """ 297set -eu 298set +o pipefail 299 300function test_nopipefail() { 301 wrong_command | cat 302 echo after 303} 304 305run_suite "bash errexit tests" 306""") 307 308 result = self.execute_test("thing.sh") 309 result.assertSuccess("bash errexit tests") 310 result.assertTestPassed("test_nopipefail") 311 result.assertLogMessage("wrong_command: command not found") 312 result.assertLogMessage("after") 313 314 def test_set_bash_errexit_pipefail_long_testname_succeeds(self): 315 test_name = "x" * 1000 316 self.write_file( 317 "thing.sh", """ 318set -euo pipefail 319 320function test_%s() { 321 : 322} 323 324run_suite "bash errexit tests" 325""" % test_name) 326 327 result = self.execute_test("thing.sh") 328 result.assertSuccess("bash errexit tests") 329 330 def test_empty_test_fails(self): 331 self.write_file("thing.sh", """ 332# No tests present. 333 334run_suite "empty test suite" 335""") 336 337 result = self.execute_test("thing.sh") 338 result.assertNotSuccess("empty test suite") 339 result.assertLogMessage("No tests found.") 340 341 def test_empty_test_succeeds_sharding(self): 342 self.write_file( 343 "thing.sh", """ 344# Only one test. 345function test_thing() { 346 echo 347} 348 349run_suite "empty test suite" 350""") 351 352 # First shard. 353 result = self.execute_test( 354 "thing.sh", env={ 355 "TEST_TOTAL_SHARDS": 2, 356 "TEST_SHARD_INDEX": 0, 357 }) 358 result.assertSuccess("empty test suite") 359 result.assertLogMessage("No tests executed due to sharding") 360 361 # Second shard. 362 result = self.execute_test( 363 "thing.sh", env={ 364 "TEST_TOTAL_SHARDS": 2, 365 "TEST_SHARD_INDEX": 1, 366 }) 367 result.assertSuccess("empty test suite") 368 result.assertNotLogMessage("No tests") 369 370 def test_filter_runs_only_matching_test(self): 371 self.write_file( 372 "thing.sh", 373 textwrap.dedent(""" 374 function test_abc() { 375 : 376 } 377 378 function test_def() { 379 echo "running def" 380 } 381 382 run_suite "tests to filter" 383 """)) 384 385 result = self.execute_test( 386 "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_a*"}) 387 388 result.assertSuccess("tests to filter") 389 result.assertTestPassed("test_abc") 390 result.assertNotLogMessage("running def") 391 392 def test_filter_prefix_match_only_skips_test(self): 393 self.write_file( 394 "thing.sh", 395 textwrap.dedent(""" 396 function test_abc() { 397 echo "running abc" 398 } 399 400 run_suite "tests to filter" 401 """)) 402 403 result = self.execute_test( 404 "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_a"}) 405 406 result.assertNotSuccess("tests to filter") 407 result.assertLogMessage("No tests found.") 408 409 def test_filter_multiple_globs_runs_tests_matching_any(self): 410 self.write_file( 411 "thing.sh", 412 textwrap.dedent(""" 413 function test_abc() { 414 echo "running abc" 415 } 416 417 function test_def() { 418 echo "running def" 419 } 420 421 run_suite "tests to filter" 422 """)) 423 424 result = self.execute_test( 425 "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "donotmatch:*a*"}) 426 427 result.assertSuccess("tests to filter") 428 result.assertTestPassed("test_abc") 429 result.assertNotLogMessage("running def") 430 431 def test_filter_character_group_runs_only_matching_tests(self): 432 self.write_file( 433 "thing.sh", 434 textwrap.dedent(""" 435 function test_aaa() { 436 : 437 } 438 439 function test_daa() { 440 : 441 } 442 443 function test_zaa() { 444 echo "running zaa" 445 } 446 447 run_suite "tests to filter" 448 """)) 449 450 result = self.execute_test( 451 "thing.sh", env={"TESTBRIDGE_TEST_ONLY": "test_[a-f]aa"}) 452 453 result.assertSuccess("tests to filter") 454 result.assertTestPassed("test_aaa") 455 result.assertTestPassed("test_daa") 456 result.assertNotLogMessage("running zaa") 457 458 def test_filter_sharded_runs_subset_of_filtered_tests(self): 459 for index in range(2): 460 with self.subTest(index=index): 461 self.__filter_sharded_runs_subset_of_filtered_tests(index) 462 463 def __filter_sharded_runs_subset_of_filtered_tests(self, index): 464 self.write_file( 465 "thing.sh", 466 textwrap.dedent(""" 467 function test_a0() { 468 echo "running a0" 469 } 470 471 function test_a1() { 472 echo "running a1" 473 } 474 475 function test_bb() { 476 echo "running bb" 477 } 478 479 run_suite "tests to filter" 480 """)) 481 482 result = self.execute_test( 483 "thing.sh", 484 env={ 485 "TESTBRIDGE_TEST_ONLY": "test_a*", 486 "TEST_TOTAL_SHARDS": 2, 487 "TEST_SHARD_INDEX": index 488 }) 489 490 result.assertSuccess("tests to filter") 491 # The sharding logic is shifted by 1, starts with 2nd shard. 492 result.assertTestPassed("test_a" + str(index ^ 1)) 493 result.assertLogMessage("running a" + str(index ^ 1)) 494 result.assertNotLogMessage("running a" + str(index)) 495 result.assertNotLogMessage("running bb") 496 497 def test_arg_runs_only_matching_test_and_issues_warning(self): 498 self.write_file( 499 "thing.sh", 500 textwrap.dedent(""" 501 function test_abc() { 502 : 503 } 504 505 function test_def() { 506 echo "running def" 507 } 508 509 run_suite "tests to filter" 510 """)) 511 512 result = self.execute_test("thing.sh", args=["test_abc"]) 513 514 result.assertSuccess("tests to filter") 515 result.assertTestPassed("test_abc") 516 result.assertNotLogMessage("running def") 517 result.assertLogMessage( 518 r"WARNING: Passing test names in arguments \(--test_arg\) is " 519 "deprecated, please use --test_filter='test_abc' instead.") 520 521 def test_arg_multiple_tests_issues_warning_with_test_filter_command(self): 522 self.write_file( 523 "thing.sh", 524 textwrap.dedent(""" 525 function test_abc() { 526 : 527 } 528 529 function test_def() { 530 : 531 } 532 533 run_suite "tests to filter" 534 """)) 535 536 result = self.execute_test("thing.sh", args=["test_abc", "test_def"]) 537 538 result.assertSuccess("tests to filter") 539 result.assertTestPassed("test_abc") 540 result.assertTestPassed("test_def") 541 result.assertLogMessage( 542 r"WARNING: Passing test names in arguments \(--test_arg\) is " 543 "deprecated, please use --test_filter='test_abc:test_def' instead.") 544 545 def test_arg_and_filter_ignores_arg(self): 546 self.write_file( 547 "thing.sh", 548 textwrap.dedent(""" 549 function test_abc() { 550 : 551 } 552 553 function test_def() { 554 echo "running def" 555 } 556 557 run_suite "tests to filter" 558 """)) 559 560 result = self.execute_test( 561 "thing.sh", args=["test_def"], env={"TESTBRIDGE_TEST_ONLY": "test_a*"}) 562 563 result.assertSuccess("tests to filter") 564 result.assertTestPassed("test_abc") 565 result.assertNotLogMessage("running def") 566 result.assertLogMessage( 567 "WARNING: Both --test_arg and --test_filter specified, ignoring --test_arg" 568 ) 569 570 def test_custom_ifs_variable_finds_and_runs_test(self): 571 for sharded in (False, True): 572 for ifs in (r"\t", "t"): 573 with self.subTest(ifs=ifs, sharded=sharded): 574 self.__custom_ifs_variable_finds_and_runs_test(ifs, sharded) 575 576 def __custom_ifs_variable_finds_and_runs_test(self, ifs, sharded): 577 self.write_file( 578 "thing.sh", 579 textwrap.dedent(r""" 580 set -euo pipefail 581 IFS=$'%s' 582 function test_foo() { 583 : 584 } 585 586 run_suite "custom IFS test" 587 """ % ifs)) 588 589 result = self.execute_test( 590 "thing.sh", 591 env={} if not sharded else { 592 "TEST_TOTAL_SHARDS": 2, 593 "TEST_SHARD_INDEX": 1 594 }) 595 596 result.assertSuccess("custom IFS test") 597 result.assertTestPassed("test_foo") 598 599 def test_fail_in_teardown_reports_failure(self): 600 self.write_file( 601 "thing.sh", 602 textwrap.dedent(r""" 603 function tear_down() { 604 echo "tear_down log" >"${TEST_log}" 605 fail "tear_down failure" 606 } 607 608 function test_foo() { 609 : 610 } 611 612 run_suite "Failure in tear_down test" 613 """)) 614 615 result = self.execute_test("thing.sh") 616 617 result.assertNotSuccess("Failure in tear_down test", errors=1) 618 result.assertTestFailed("test_foo", "tear_down failure") 619 result.assertXmlMessage('message="tear_down failure"') 620 result.assertLogMessage("tear_down log") 621 622 def test_fail_in_teardown_after_test_failure_reports_both_failures(self): 623 self.write_file( 624 "thing.sh", 625 textwrap.dedent(r""" 626 function tear_down() { 627 echo "tear_down log" >"${TEST_log}" 628 fail "tear_down failure" 629 } 630 631 function test_foo() { 632 echo "test_foo log" >"${TEST_log}" 633 fail "Test failure" 634 } 635 636 run_suite "Failure in tear_down test" 637 """)) 638 639 result = self.execute_test("thing.sh") 640 641 result.assertNotSuccess("Failure in tear_down test", errors=1) 642 result.assertTestFailed("test_foo", "Test failure") 643 result.assertTestFailed("test_foo", "tear_down failure") 644 result.assertXmlMessage('message="Test failure"') 645 result.assertNotXmlMessage('message="tear_down failure"') 646 result.assertXmlMessage("test_foo log") 647 result.assertXmlMessage("tear_down log") 648 result.assertLogMessage("Test failure") 649 result.assertLogMessage("tear_down failure") 650 result.assertLogMessage("test_foo log") 651 result.assertLogMessage("tear_down log") 652 653 def test_errexit_in_teardown_reports_failure(self): 654 self.write_file( 655 "thing.sh", 656 textwrap.dedent(r""" 657 set -euo pipefail 658 659 function tear_down() { 660 invalid_command 661 } 662 663 function test_foo() { 664 : 665 } 666 667 run_suite "errexit in tear_down test" 668 """)) 669 670 result = self.execute_test("thing.sh") 671 672 result.assertNotSuccess("errexit in tear_down test") 673 result.assertLogMessage("invalid_command: command not found") 674 result.assertXmlMessage('message="No failure message"') 675 result.assertXmlMessage("invalid_command: command not found") 676 677 def test_fail_in_tear_down_after_errexit_reports_both_failures(self): 678 self.write_file( 679 "thing.sh", 680 textwrap.dedent(r""" 681 set -euo pipefail 682 683 function tear_down() { 684 echo "tear_down log" >"${TEST_log}" 685 fail "tear_down failure" 686 } 687 688 function test_foo() { 689 invalid_command 690 } 691 692 run_suite "fail after failure" 693 """)) 694 695 result = self.execute_test("thing.sh") 696 697 result.assertNotSuccess("fail after failure") 698 result.assertTestFailed( 699 "test_foo", 700 "terminated because this command returned a non-zero status") 701 result.assertTestFailed("test_foo", "tear_down failure") 702 result.assertLogMessage("invalid_command: command not found") 703 result.assertLogMessage("tear_down log") 704 result.assertXmlMessage('message="No failure message"') 705 result.assertXmlMessage("invalid_command: command not found") 706 707 def test_errexit_in_tear_down_after_errexit_reports_both_failures(self): 708 self.write_file( 709 "thing.sh", 710 textwrap.dedent(r""" 711 set -euo pipefail 712 713 function tear_down() { 714 invalid_command_tear_down 715 } 716 717 function test_foo() { 718 invalid_command_test 719 } 720 721 run_suite "fail after failure" 722 """)) 723 724 result = self.execute_test("thing.sh") 725 726 result.assertNotSuccess("fail after failure") 727 result.assertTestFailed( 728 "test_foo", 729 "terminated because this command returned a non-zero status") 730 result.assertLogMessage("invalid_command_test: command not found") 731 result.assertLogMessage("invalid_command_tear_down: command not found") 732 result.assertXmlMessage('message="No failure message"') 733 result.assertXmlMessage("invalid_command_test: command not found") 734 result.assertXmlMessage("invalid_command_tear_down: command not found") 735 736 737if __name__ == "__main__": 738 unittest.main() 739