| # Copyright 2014 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Test the failures_lib module.""" |
| |
| import json |
| |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_test_lib |
| from chromite.lib import failure_message_lib |
| from chromite.lib import failures_lib |
| |
| |
| class StepFailureTests(cros_test_lib.TestCase): |
| """Tests for StepFailure.""" |
| |
| def testConvertToStageFailureMessage(self) -> None: |
| """Test ConvertToStageFailureMessage.""" |
| failure = failures_lib.StepFailure("step failure message") |
| stage_failure_msg = failure.ConvertToStageFailureMessage( |
| 1, "HWTest [sanity]" |
| ) |
| self.assertEqual(stage_failure_msg.stage_name, "HWTest [sanity]") |
| self.assertEqual(stage_failure_msg.stage_prefix_name, "HWTest") |
| self.assertEqual(stage_failure_msg.exception_type, "StepFailure") |
| self.assertEqual(stage_failure_msg.exception_category, "unknown") |
| |
| |
| class CompoundFailureTest(cros_test_lib.TestCase): |
| """Test the CompoundFailure class.""" |
| |
| def _CreateExceptInfos(self, cls, message="", traceback="", num=1): |
| """A helper function to create a list of ExceptInfo objects.""" |
| exc_infos = [] |
| for _ in range(num): |
| exc_infos.extend( |
| failures_lib.CreateExceptInfo(cls(message), traceback) |
| ) |
| |
| return exc_infos |
| |
| def testHasEmptyList(self) -> None: |
| """Tests the HasEmptyList method.""" |
| self.assertTrue(failures_lib.CompoundFailure().HasEmptyList()) |
| exc_infos = self._CreateExceptInfos(KeyError) |
| self.assertFalse( |
| failures_lib.CompoundFailure(exc_infos=exc_infos).HasEmptyList() |
| ) |
| |
| def testHasAndMatchesFailureType(self) -> None: |
| """Tests the HasFailureType and the MatchesFailureType methods.""" |
| # Create a CompoundFailure instance with mixed types of exceptions. |
| exc_infos = self._CreateExceptInfos(KeyError) |
| exc_infos.extend(self._CreateExceptInfos(ValueError)) |
| exc = failures_lib.CompoundFailure(exc_infos=exc_infos) |
| self.assertTrue(exc.HasFailureType(KeyError)) |
| self.assertTrue(exc.HasFailureType(ValueError)) |
| self.assertFalse(exc.MatchesFailureType(KeyError)) |
| self.assertFalse(exc.MatchesFailureType(ValueError)) |
| |
| # Create a CompoundFailure instance with a single type of exceptions. |
| exc_infos = self._CreateExceptInfos(KeyError, num=5) |
| exc = failures_lib.CompoundFailure(exc_infos=exc_infos) |
| self.assertTrue(exc.HasFailureType(KeyError)) |
| self.assertFalse(exc.HasFailureType(ValueError)) |
| self.assertTrue(exc.MatchesFailureType(KeyError)) |
| self.assertFalse(exc.MatchesFailureType(ValueError)) |
| |
| def testHasFatalFailure(self) -> None: |
| """Tests the HasFatalFailure method.""" |
| exc_infos = self._CreateExceptInfos(KeyError) |
| exc_infos.extend(self._CreateExceptInfos(ValueError)) |
| exc = failures_lib.CompoundFailure(exc_infos=exc_infos) |
| self.assertTrue(exc.HasFatalFailure()) |
| self.assertTrue(exc.HasFatalFailure(exempt_exception_list=[KeyError])) |
| self.assertFalse( |
| exc.HasFatalFailure(exempt_exception_list=[KeyError, ValueError]) |
| ) |
| |
| exc = failures_lib.CompoundFailure() |
| self.assertFalse(exc.HasFatalFailure()) |
| |
| def testMessageContainsAllInfo(self) -> None: |
| """Tests that by default, all information is included in the message.""" |
| exc_infos = self._CreateExceptInfos( |
| KeyError, message="bar1", traceback="foo1" |
| ) |
| exc_infos.extend( |
| self._CreateExceptInfos( |
| ValueError, message="bar2", traceback="foo2" |
| ) |
| ) |
| exc = failures_lib.CompoundFailure(exc_infos=exc_infos) |
| self.assertIn("bar1", str(exc)) |
| self.assertIn("bar2", str(exc)) |
| self.assertIn("KeyError", str(exc)) |
| self.assertIn("ValueError", str(exc)) |
| self.assertIn("foo1", str(exc)) |
| self.assertIn("foo2", str(exc)) |
| |
| def testConvertToStageFailureMessage(self) -> None: |
| """Test ConvertToStageFailureMessage.""" |
| exc_infos = self._CreateExceptInfos( |
| KeyError, message="bar1", traceback="foo1" |
| ) |
| exc_infos.extend( |
| self._CreateExceptInfos( |
| failures_lib.StepFailure, message="bar2", traceback="foo2" |
| ) |
| ) |
| exc = failures_lib.CompoundFailure( |
| message="compound failure", exc_infos=exc_infos |
| ) |
| stage_failure_msg = exc.ConvertToStageFailureMessage( |
| 1, "HWTest [sanity]" |
| ) |
| |
| self.assertEqual(len(stage_failure_msg.inner_failures), 2) |
| self.assertEqual(stage_failure_msg.stage_name, "HWTest [sanity]") |
| self.assertEqual(stage_failure_msg.stage_prefix_name, "HWTest") |
| self.assertEqual(stage_failure_msg.exception_type, "CompoundFailure") |
| self.assertEqual(stage_failure_msg.exception_category, "unknown") |
| |
| |
| class SetFailureTypeTest(cros_test_lib.TestCase): |
| """Test that the SetFailureType decorator works.""" |
| |
| ERROR_MESSAGE = "You failed!" |
| |
| class TacoNotTasty(failures_lib.CompoundFailure): |
| """Raised when the taco is not tasty.""" |
| |
| class NoGuacamole(TacoNotTasty): |
| """Raised when no guacamole in the taco.""" |
| |
| class SubparLunch(failures_lib.CompoundFailure): |
| """Raised when the lunch is subpar.""" |
| |
| class FooException(Exception): |
| """A foo exception.""" |
| |
| def _GetFunction(self, set_type, raise_type, *args, **kwargs): |
| """Returns a function to test. |
| |
| Args: |
| set_type: The exception type that the function is decorated with. |
| raise_type: The exception type that the function raises. |
| *args: args to pass to the instance of |raise_type|. |
| |
| Returns: |
| The function to test. |
| """ |
| |
| @failures_lib.SetFailureType(set_type) |
| def f() -> None: |
| raise raise_type(*args, **kwargs) |
| |
| return f |
| |
| def testAssertionFailOnIllegalExceptionType(self) -> None: |
| """Assertion should fail if the pre-set type is not allowed .""" |
| self.assertRaises( |
| AssertionError, self._GetFunction, ValueError, self.FooException |
| ) |
| |
| def testReraiseAsNewException(self) -> None: |
| """Tests that the pre-set exception type is raised correctly.""" |
| try: |
| self._GetFunction( |
| self.TacoNotTasty, self.FooException, self.ERROR_MESSAGE |
| )() |
| except Exception as e: |
| self.assertIsInstance(e, self.TacoNotTasty) |
| self.assertTrue(e.msg, self.ERROR_MESSAGE) |
| self.assertEqual(len(e.exc_infos), 1) |
| self.assertEqual(e.exc_infos[0].str, self.ERROR_MESSAGE) |
| self.assertEqual(e.exc_infos[0].type, self.FooException) |
| self.assertIsInstance(e.exc_infos[0].traceback, str) |
| |
| def testReraiseACompoundFailure(self) -> None: |
| """Tests that the list of ExceptInfo objects are copied over.""" |
| tb1 = "Stub traceback1" |
| tb2 = "Stub traceback2" |
| org_infos = failures_lib.CreateExceptInfo( |
| ValueError("No taco."), tb1 |
| ) + failures_lib.CreateExceptInfo(OSError("No salsa"), tb2) |
| try: |
| self._GetFunction( |
| self.SubparLunch, self.TacoNotTasty, exc_infos=org_infos |
| )() |
| except Exception as e: |
| self.assertIsInstance(e, self.SubparLunch) |
| # The orignal exceptions stored in exc_infos are preserved. |
| self.assertEqual(e.exc_infos, org_infos) |
| # All essential inforamtion should be included in the message of |
| # the new excpetion. |
| self.assertIn(tb1, str(e)) |
| self.assertIn(tb2, str(e)) |
| self.assertIn(str(ValueError), str(e)) |
| self.assertIn(str(OSError), str(e)) |
| self.assertIn(str("No taco"), str(e)) |
| self.assertIn(str("No salsa"), str(e)) |
| |
| # Assert that summary does not contain the textual tracebacks. |
| self.assertFalse(tb1 in e.ToSummaryString()) |
| self.assertFalse(tb2 in e.ToSummaryString()) |
| |
| def testReraiseACompoundFailureWithEmptyList(self) -> None: |
| """Tests that a CompoundFailure with empty list is handled correctly.""" |
| try: |
| self._GetFunction( |
| self.SubparLunch, self.TacoNotTasty, message="empty list" |
| )() |
| except Exception as e: |
| self.assertIsInstance(e, self.SubparLunch) |
| self.assertEqual(e.exc_infos[0].type, self.TacoNotTasty) |
| |
| def testReraiseOriginalException(self) -> None: |
| """Tests that the original exception is re-raised.""" |
| # NoGuacamole is a subclass of TacoNotTasty, so the wrapper has no |
| # effect on it. |
| f = self._GetFunction(self.TacoNotTasty, self.NoGuacamole) |
| self.assertRaises(self.NoGuacamole, f) |
| |
| def testPassArgsToWrappedFunctor(self) -> None: |
| """Tests that we can pass arguments to the functor.""" |
| |
| @failures_lib.SetFailureType(self.TacoNotTasty) |
| def f(arg): |
| return arg |
| |
| @failures_lib.SetFailureType(self.TacoNotTasty) |
| def g(kwarg=""): |
| return kwarg |
| |
| # Test passing arguments. |
| self.assertEqual(f("foo"), "foo") |
| # Test passing keyword arguments. |
| self.assertEqual(g(kwarg="bar"), "bar") |
| |
| |
| class ExceptInfoTest(cros_test_lib.TestCase): |
| """Tests the namedtuple class ExceptInfo.""" |
| |
| def testConvertToExceptInfo(self) -> None: |
| """Tests converting an exception to an ExceptInfo object.""" |
| traceback = "Stub traceback" |
| message = "Taco is not a valid option!" |
| except_infos = failures_lib.CreateExceptInfo( |
| ValueError(message), traceback |
| ) |
| |
| self.assertEqual(except_infos[0].type, ValueError) |
| self.assertEqual(except_infos[0].str, message) |
| self.assertEqual(except_infos[0].traceback, traceback) |
| |
| |
| class FailureTypeListTests(cros_test_lib.TestCase): |
| """Tests for failure type lists.""" |
| |
| def testFailureTypeList(self) -> None: |
| """Verify current failure names are already added to the type lists.""" |
| self.assertTrue( |
| failures_lib.BuildScriptFailure.__name__ |
| in failure_message_lib.BUILD_SCRIPT_FAILURE_TYPES |
| ) |
| self.assertTrue( |
| failures_lib.PackageBuildFailure.__name__ |
| in failure_message_lib.PACKAGE_BUILD_FAILURE_TYPES |
| ) |
| |
| |
| class GetStageFailureMessageFromExceptionTests(cros_test_lib.TestCase): |
| """Tests for GetStageFailureMessageFromException""" |
| |
| def testGetStageFailureMessageFromExceptionOnStepFailure(self) -> None: |
| """Test GetStageFailureMessageFromException on StepFailure.""" |
| exc = failures_lib.StepFailure("step failure message") |
| msg = failures_lib.GetStageFailureMessageFromException( |
| "CommitQueueSync", 1, exc |
| ) |
| self.assertEqual(msg.build_stage_id, 1) |
| self.assertEqual(msg.stage_name, "CommitQueueSync") |
| self.assertEqual(msg.stage_prefix_name, "CommitQueueSync") |
| self.assertEqual(msg.exception_type, "StepFailure") |
| self.assertEqual(msg.exception_category, "unknown") |
| |
| def testGetStageFailureMessageFromExceptionOnException(self) -> None: |
| """Test GetStageFailureMessageFromException on regular exception.""" |
| exc = ValueError("Invalid valure.") |
| msg = failures_lib.GetStageFailureMessageFromException( |
| "CommitQueueSync", 1, exc |
| ) |
| self.assertEqual(msg.build_stage_id, 1) |
| self.assertEqual(msg.stage_name, "CommitQueueSync") |
| self.assertEqual(msg.stage_prefix_name, "CommitQueueSync") |
| self.assertEqual(msg.exception_type, "ValueError") |
| self.assertEqual(msg.exception_category, "unknown") |
| |
| |
| class BuildFailuresForFindit(cros_test_lib.TestCase): |
| """Test cases for exporting build failures for Findit integration.""" |
| |
| def testBuildFailuresJson(self) -> None: |
| error = cros_build_lib.RunCommandError("run cmd error") |
| failed_packages = ["sys-apps/mosys", "chromeos-base/cryptohome"] |
| build_failure = failures_lib.PackageBuildFailure( |
| error, "build_packages", failed_packages |
| ) |
| self.assertSetEqual(set(failed_packages), build_failure.failed_packages) |
| failure_json = build_failure.BuildCompileFailureOutputJson() |
| values = json.loads(failure_json) |
| failures = values["failures"] |
| self.assertEqual(len(failures), 2) |
| # Verify both output targets are not equal, this makes sure the loop |
| # below is correct. |
| self.assertNotEqual( |
| failures[0]["output_targets"], failures[1]["output_targets"] |
| ) |
| for value in failures: |
| self.assertEqual(value["rule"], "emerge") |
| self.assertIn(value["output_targets"], failed_packages) |