xref: /aosp_15_r20/external/bazelbuild-rules_android/test/bashunit/unittest_test.py (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
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 &lt;&gt;&amp;&quot; 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