Allow running `fail` from test `tear_down` function.

The `fail` function runs `tear_down` in order to make sure we perform cleanup after a failed test. This creates a problem if the `tear_down` function uses the `fail` function, in which case we would enter infinite recursion.

Add a marker variable to prevent `fail` from running `tear_down` once we reach the `tear_down` phase. In case both the test and `tear_down` fail, keep the failure message from the test in the `test.xml` file and concatenate logs from both.

Add stricter checks for logs checked in `bashunit` test that the logs happen exactly once and migrate `%` based string formats to use f-strings where it aids readability.

PiperOrigin-RevId: 428872745
diff --git a/src/test/shell/unittest_test.py b/src/test/shell/unittest_test.py
index b311a27..f03bda3 100644
--- a/src/test/shell/unittest_test.py
+++ b/src/test/shell/unittest_test.py
@@ -74,36 +74,44 @@
   # Methods to assert on the state of the results.
 
   def assertLogMessage(self, message):
-    self._asserter.assertRegex(self._output, message)
+    self.assertExactlyOneMatch(self._output, message)
 
   def assertNotLogMessage(self, message):
     self._asserter.assertNotRegex(self._output, message)
 
   def assertXmlMessage(self, message):
-    self._asserter.assertRegex(self._xml, message)
+    self.assertExactlyOneMatch(self._xml, message)
+
+  def assertNotXmlMessage(self, message):
+    self._asserter.assertNotRegex(self._xml, message)
 
   def assertSuccess(self, suite_name):
     self._asserter.assertEqual(0, self._return_code,
-                               "Script failed unexpectedly:\n%s" % self._output)
+                               f"Script failed unexpectedly:\n{self._output}")
     self.assertLogMessage(suite_name)
-    self.assertXmlMessage("failures=\"0\"")
-    self.assertXmlMessage("errors=\"0\"")
+    self.assertXmlMessage("<testsuites [^/]*failures=\"0\"")
+    self.assertXmlMessage("<testsuites [^/]*errors=\"0\"")
 
   def assertNotSuccess(self, suite_name, failures=0, errors=0):
     self._asserter.assertNotEqual(0, self._return_code)
     self.assertLogMessage(suite_name)
     if failures:
-      self.assertXmlMessage("failures=\"%d\"" % failures)
+      self.assertXmlMessage(f'<testsuites [^/]*failures="{failures}"')
     if errors:
-      self.assertXmlMessage("errors=\"%d\"" % errors)
+      self.assertXmlMessage(f'<testsuites [^/]*errors="{errors}"')
 
   def assertTestPassed(self, test_name):
-    self.assertLogMessage("PASSED: %s" % test_name)
+    self.assertLogMessage(f"PASSED: {test_name}")
 
   def assertTestFailed(self, test_name, message=""):
-    self.assertLogMessage("%s FAILED" % test_name)
-    if message:
-      self.assertLogMessage("FAILED: %s" % message)
+    self.assertLogMessage(f"{test_name} FAILED: {message}")
+
+  def assertExactlyOneMatch(self, text, pattern):
+    self._asserter.assertRegex(text, pattern)
+    self._asserter.assertEqual(
+        len(re.findall(pattern, text)),
+        1,
+        msg=f"Found more than 1 match of '{pattern}' in '{text}'")
 
 
 class UnittestTest(unittest.TestCase):
@@ -136,7 +144,7 @@
       return os.environ["TEST_SRCDIR"]
 
     # Base on the current dir
-    return "%s/.." % os.getcwd()
+    return f"{os.getcwd()}/.."
 
   def execute_test(self, filename, env=None, args=()):
     """Executes the file and stores the results."""
@@ -585,8 +593,147 @@
             "TEST_TOTAL_SHARDS": 2,
             "TEST_SHARD_INDEX": 1
         })
+
     result.assertSuccess("custom IFS test")
     result.assertTestPassed("test_foo")
 
+  def test_fail_in_teardown_reports_failure(self):
+    self.write_file(
+        "thing.sh",
+        textwrap.dedent(r"""
+        function tear_down() {
+          echo "tear_down log" >"${TEST_log}"
+          fail "tear_down failure"
+        }
+
+        function test_foo() {
+          :
+        }
+
+        run_suite "Failure in tear_down test"
+        """))
+
+    result = self.execute_test("thing.sh")
+
+    result.assertNotSuccess("Failure in tear_down test", errors=1)
+    result.assertTestFailed("test_foo", "tear_down failure")
+    result.assertXmlMessage('message="tear_down failure"')
+    result.assertLogMessage("tear_down log")
+
+  def test_fail_in_teardown_after_test_failure_reports_both_failures(self):
+    self.write_file(
+        "thing.sh",
+        textwrap.dedent(r"""
+        function tear_down() {
+          echo "tear_down log" >"${TEST_log}"
+          fail "tear_down failure"
+        }
+
+        function test_foo() {
+          echo "test_foo log" >"${TEST_log}"
+          fail "Test failure"
+        }
+
+        run_suite "Failure in tear_down test"
+        """))
+
+    result = self.execute_test("thing.sh")
+
+    result.assertNotSuccess("Failure in tear_down test", errors=1)
+    result.assertTestFailed("test_foo", "Test failure")
+    result.assertTestFailed("test_foo", "tear_down failure")
+    result.assertXmlMessage('message="Test failure"')
+    result.assertNotXmlMessage('message="tear_down failure"')
+    result.assertXmlMessage("test_foo log")
+    result.assertXmlMessage("tear_down log")
+    result.assertLogMessage("Test failure")
+    result.assertLogMessage("tear_down failure")
+    result.assertLogMessage("test_foo log")
+    result.assertLogMessage("tear_down log")
+
+  def test_errexit_in_teardown_reports_failure(self):
+    self.write_file(
+        "thing.sh",
+        textwrap.dedent(r"""
+        set -euo pipefail
+
+        function tear_down() {
+          invalid_command
+        }
+
+        function test_foo() {
+          :
+        }
+
+        run_suite "errexit in tear_down test"
+        """))
+
+    result = self.execute_test("thing.sh")
+
+    result.assertNotSuccess("errexit in tear_down test")
+    result.assertLogMessage("invalid_command: command not found")
+    result.assertXmlMessage('message="No failure message"')
+    result.assertXmlMessage("invalid_command: command not found")
+
+  def test_fail_in_tear_down_after_errexit_reports_both_failures(self):
+    self.write_file(
+        "thing.sh",
+        textwrap.dedent(r"""
+        set -euo pipefail
+
+        function tear_down() {
+          echo "tear_down log" >"${TEST_log}"
+          fail "tear_down failure"
+        }
+
+        function test_foo() {
+          invalid_command
+        }
+
+        run_suite "fail after failure"
+        """))
+
+    result = self.execute_test("thing.sh")
+
+    result.assertNotSuccess("fail after failure")
+    result.assertTestFailed(
+        "test_foo",
+        "terminated because this command returned a non-zero status")
+    result.assertTestFailed("test_foo", "tear_down failure")
+    result.assertLogMessage("invalid_command: command not found")
+    result.assertLogMessage("tear_down log")
+    result.assertXmlMessage('message="No failure message"')
+    result.assertXmlMessage("invalid_command: command not found")
+
+  def test_errexit_in_tear_down_after_errexit_reports_both_failures(self):
+    self.write_file(
+        "thing.sh",
+        textwrap.dedent(r"""
+        set -euo pipefail
+
+        function tear_down() {
+          invalid_command_tear_down
+        }
+
+        function test_foo() {
+          invalid_command_test
+        }
+
+        run_suite "fail after failure"
+        """))
+
+    result = self.execute_test("thing.sh")
+
+    result.assertNotSuccess("fail after failure")
+    result.assertTestFailed(
+        "test_foo",
+        "terminated because this command returned a non-zero status")
+    result.assertLogMessage("invalid_command_test: command not found")
+    result.assertLogMessage("invalid_command_tear_down: command not found")
+    result.assertXmlMessage('message="No failure message"')
+    result.assertXmlMessage("invalid_command_test: command not found")
+    result.assertXmlMessage("invalid_command_tear_down: command not found")
+
+
 if __name__ == "__main__":
   unittest.main()