| # -*- coding: utf-8 -*- |
| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Unittests for the stage results.""" |
| |
| from __future__ import print_function |
| |
| import os |
| import signal |
| import sys |
| import time |
| |
| import mock |
| from six.moves import StringIO |
| |
| from chromite.lib import constants |
| from chromite.lib import config_lib_unittest |
| from chromite.lib import failures_lib |
| from chromite.lib import results_lib |
| from chromite.cbuildbot import cbuildbot_run |
| from chromite.cbuildbot.builders import simple_builders |
| from chromite.cbuildbot.stages import generic_stages |
| from chromite.lib import cidb |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import cros_test_lib |
| from chromite.lib import fake_cidb |
| from chromite.lib import parallel |
| from chromite.lib.buildstore import FakeBuildStore |
| |
| |
| assert sys.version_info >= (3, 6), 'This module requires Python 3.6+' |
| |
| |
| class PassStage(generic_stages.BuilderStage): |
| """PassStage always works""" |
| |
| |
| class Pass2Stage(generic_stages.BuilderStage): |
| """Pass2Stage always works""" |
| |
| |
| class FailStage(generic_stages.BuilderStage): |
| """FailStage always throws an exception""" |
| |
| FAIL_EXCEPTION = failures_lib.StepFailure('Fail stage needs to fail.') |
| |
| def PerformStage(self): |
| """Throw the exception to make us fail.""" |
| raise self.FAIL_EXCEPTION |
| |
| |
| class SneakyFailStage(generic_stages.BuilderStage): |
| """SneakyFailStage exits with an error.""" |
| |
| def PerformStage(self): |
| """Exit without reporting back.""" |
| # pylint: disable=protected-access |
| os._exit(1) |
| |
| |
| class SuicideStage(generic_stages.BuilderStage): |
| """SuicideStage kills itself with kill -9.""" |
| |
| def PerformStage(self): |
| """Exit without reporting back.""" |
| os.kill(os.getpid(), signal.SIGKILL) |
| |
| |
| class SetAttrStage(generic_stages.BuilderStage): |
| """Stage that sets requested run attribute to a value.""" |
| |
| DEFAULT_ATTR = 'unittest_value' |
| VALUE = 'HereTakeThis' |
| |
| def __init__(self, |
| builder_run, |
| buildstore, |
| delay=2, |
| attr=DEFAULT_ATTR, |
| *args, |
| **kwargs): |
| super(SetAttrStage, self).__init__(builder_run, buildstore, *args, **kwargs) |
| self.delay = delay |
| self.attr = attr |
| |
| def PerformStage(self): |
| """Wait self.delay seconds then set requested run attribute.""" |
| time.sleep(self.delay) |
| self._run.attrs.SetParallel(self.attr, self.VALUE) |
| |
| def QueueableException(self): |
| return cbuildbot_run.ParallelAttributeError(self.attr) |
| |
| |
| class GetAttrStage(generic_stages.BuilderStage): |
| """Stage that accesses requested run attribute and confirms value.""" |
| |
| DEFAULT_ATTR = 'unittest_value' |
| |
| def __init__(self, |
| builder_run, |
| buildstore, |
| tester=None, |
| timeout=5, |
| attr=DEFAULT_ATTR, |
| *args, |
| **kwargs): |
| super(GetAttrStage, self).__init__(builder_run, buildstore, *args, **kwargs) |
| self.tester = tester |
| self.timeout = timeout |
| self.attr = attr |
| |
| def PerformStage(self): |
| """Wait for attrs.test value to show up.""" |
| assert not self._run.attrs.HasParallel(self.attr) |
| value = self._run.attrs.GetParallel(self.attr, self.timeout) |
| if self.tester: |
| self.tester(value) |
| |
| def QueueableException(self): |
| return cbuildbot_run.ParallelAttributeError(self.attr) |
| |
| def TimeoutException(self): |
| return cbuildbot_run.AttrTimeoutError(self.attr) |
| |
| |
| class BuildStagesResultsTest(cros_test_lib.TestCase): |
| """Tests for stage results and reporting.""" |
| |
| def setUp(self): |
| # Always stub RunCommand out as we use it in every method. |
| self._bot_id = 'amd64-generic-release' |
| self.buildstore = FakeBuildStore() |
| site_config = config_lib_unittest.MockSiteConfig() |
| build_config = site_config[self._bot_id] |
| self.build_root = '/fake_root' |
| # This test compares log output from the stages, so turn on buildbot |
| # logging. |
| logging.EnableBuildbotMarkers() |
| |
| self.db = fake_cidb.FakeCIDBConnection() |
| cidb.CIDBConnectionFactory.SetupMockCidb(self.db) |
| |
| # Create a class to hold |
| class Options(object): |
| """Dummy class to hold option values.""" |
| |
| options = Options() |
| options.archive_base = 'gs://dontcare' |
| options.buildroot = self.build_root |
| options.debug = False |
| options.prebuilts = False |
| options.clobber = False |
| options.nosdk = False |
| options.remote_trybot = False |
| options.latest_toolchain = False |
| options.buildnumber = 1234 |
| options.android_rev = None |
| options.chrome_rev = None |
| options.branch = 'dontcare' |
| options.chrome_root = False |
| options.build_config_name = '' |
| |
| self._manager = parallel.Manager() |
| # Pylint-1.9 has a false positive on this for some reason. |
| self._manager.__enter__() # pylint: disable=no-value-for-parameter |
| |
| self._run = cbuildbot_run.BuilderRun(options, site_config, build_config, |
| self._manager) |
| |
| results_lib.Results.Clear() |
| |
| def tearDown(self): |
| # Mimic exiting with statement for self._manager. |
| if hasattr(self, '_manager') and self._manager is not None: |
| self._manager.__exit__(None, None, None) |
| |
| cidb.CIDBConnectionFactory.SetupMockCidb() |
| |
| def _runStages(self): |
| """Run a couple of stages so we can capture the results""" |
| # Run two pass stages, and one fail stage. |
| PassStage(self._run, self.buildstore).Run() |
| Pass2Stage(self._run, self.buildstore).Run() |
| self.assertRaises(failures_lib.StepFailure, |
| FailStage(self._run, self.buildstore).Run) |
| |
| def _verifyRunResults(self, expectedResults, max_time=2.0): |
| actualResults = results_lib.Results.Get() |
| |
| # Break out the asserts to be per item to make debugging easier |
| self.assertEqual(len(expectedResults), len(actualResults)) |
| for i, (xname, xresult) in enumerate(expectedResults): |
| entry = actualResults[i] |
| |
| if entry.result not in results_lib.Results.NON_FAILURE_TYPES: |
| self.assertTrue(isinstance(entry.result, BaseException)) |
| if isinstance(entry.result, failures_lib.StepFailure): |
| self.assertEqual(str(entry.result), entry.description) |
| |
| self.assertTrue(entry.time >= 0 and entry.time < max_time) |
| self.assertEqual(xname, entry.name) |
| self.assertEqual(type(xresult), type(entry.result)) |
| self.assertEqual(repr(xresult), repr(entry.result)) |
| |
| def _PassString(self): |
| record = results_lib.Result('Pass', results_lib.Results.SUCCESS, 'None', |
| 'Pass', '', '0') |
| return results_lib.Results.SPLIT_TOKEN.join(record) + '\n' |
| |
| def testRunStages(self): |
| """Run some stages and verify the captured results""" |
| |
| self.assertEqual(results_lib.Results.Get(), []) |
| |
| self._runStages() |
| |
| # Verify that the results are what we expect. |
| expectedResults = [ |
| ('Pass', results_lib.Results.SUCCESS), |
| ('Pass2', results_lib.Results.SUCCESS), |
| ('Fail', FailStage.FAIL_EXCEPTION), |
| ] |
| self._verifyRunResults(expectedResults) |
| |
| def testSuccessTest(self): |
| """Run some stages and verify the captured results""" |
| |
| results_lib.Results.Record('Pass', results_lib.Results.SUCCESS) |
| |
| self.assertTrue(results_lib.Results.BuildSucceededSoFar()) |
| |
| results_lib.Results.Record('Fail', FailStage.FAIL_EXCEPTION, time=1) |
| |
| self.assertFalse(results_lib.Results.BuildSucceededSoFar()) |
| |
| results_lib.Results.Record('Pass2', results_lib.Results.SUCCESS) |
| |
| self.assertFalse(results_lib.Results.BuildSucceededSoFar()) |
| |
| def testSuccessTestWithDB(self): |
| """Test BuildSucceededSoFar with DB instance""" |
| build_id = self.db.InsertBuild('builder_name', 'build_number', |
| 'build_config', 'bot_hostname', |
| buildbucket_id=1234) |
| |
| results_lib.Results.Record('stage1', results_lib.Results.SUCCESS) |
| results_lib.Results.Record('stage2', results_lib.Results.SKIPPED) |
| |
| self.db.InsertBuildStage( |
| build_id, 'stage1', status=constants.BUILDER_STATUS_PASSED) |
| self.db.InsertBuildStage( |
| build_id, 'stage2', status=constants.BUILDER_STATUS_SKIPPED) |
| self.db.InsertBuildStage( |
| build_id, 'stage3', status=constants.BUILDER_STATUS_FORGIVEN) |
| self.db.InsertBuildStage( |
| build_id, 'stage4', status=constants.BUILDER_STATUS_PLANNED) |
| |
| self.buildstore = FakeBuildStore(self.db) |
| self.assertTrue(results_lib.Results.BuildSucceededSoFar(self.buildstore, |
| 1234)) |
| |
| self.db.InsertBuildStage( |
| build_id, 'stage5', status=constants.BUILDER_STATUS_INFLIGHT) |
| |
| self.assertTrue( |
| results_lib.Results.BuildSucceededSoFar(self.buildstore, |
| 1234, 'stage5')) |
| |
| self.db.InsertBuildStage( |
| build_id, 'stage6', status=constants.BUILDER_STATUS_FAILED) |
| |
| self.assertFalse( |
| results_lib.Results.BuildSucceededSoFar(self.buildstore, |
| 1234, 'stage5')) |
| |
| def _TestParallelStages(self, stage_objs): |
| builder = simple_builders.SimpleBuilder(self._run, self.buildstore) |
| error = None |
| # pylint: disable=protected-access |
| with mock.patch.multiple(parallel._BackgroundTask, PRINT_INTERVAL=0.01): |
| try: |
| builder._RunParallelStages(stage_objs) |
| except parallel.BackgroundFailure as ex: |
| error = ex |
| |
| return error |
| |
| def testParallelStages(self): |
| bs = FakeBuildStore() |
| stage_objs = [ |
| stage(self._run, bs) for stage in (PassStage, SneakyFailStage, |
| FailStage, SuicideStage, Pass2Stage) |
| ] |
| error = self._TestParallelStages(stage_objs) |
| self.assertTrue(error) |
| expectedResults = [ |
| ('Pass', results_lib.Results.SUCCESS), |
| ('Fail', FailStage.FAIL_EXCEPTION), |
| ('Pass2', results_lib.Results.SUCCESS), |
| ('SneakyFail', error), |
| ('Suicide', error), |
| ] |
| self._verifyRunResults(expectedResults) |
| |
| def testParallelStageCommunicationOK(self): |
| """Test run attr communication betweeen parallel stages.""" |
| |
| def assert_test(value): |
| self.assertEqual( |
| value, SetAttrStage.VALUE, |
| 'Expected value %r to be passed between stages, but' |
| ' got %r.' % (SetAttrStage.VALUE, value)) |
| |
| bs = FakeBuildStore() |
| stage_objs = [ |
| SetAttrStage(self._run, bs), |
| GetAttrStage(self._run, bs, assert_test, timeout=30), |
| GetAttrStage(self._run, bs, assert_test, timeout=30), |
| ] |
| error = self._TestParallelStages(stage_objs) |
| self.assertFalse(error) |
| expectedResults = [ |
| ('SetAttr', results_lib.Results.SUCCESS), |
| ('GetAttr', results_lib.Results.SUCCESS), |
| ('GetAttr', results_lib.Results.SUCCESS), |
| ] |
| self._verifyRunResults(expectedResults, max_time=90.0) |
| |
| # Make sure run attribute propagated up to the top, too. |
| value = self._run.attrs.GetParallel('unittest_value') |
| self.assertEqual(SetAttrStage.VALUE, value) |
| |
| def testParallelStageCommunicationTimeout(self): |
| """Test run attr communication between parallel stages that times out.""" |
| |
| def assert_test(value): |
| self.assertEqual( |
| value, SetAttrStage.VALUE, |
| 'Expected value %r to be passed between stages, but' |
| ' got %r.' % (SetAttrStage.VALUE, value)) |
| |
| bs = FakeBuildStore() |
| stage_objs = [ |
| SetAttrStage(self._run, bs, delay=11), |
| GetAttrStage(self._run, bs, assert_test, timeout=1), |
| ] |
| error = self._TestParallelStages(stage_objs) |
| self.assertTrue(error) |
| expectedResults = [ |
| ('SetAttr', results_lib.Results.SUCCESS), |
| ('GetAttr', stage_objs[1].TimeoutException()), |
| ] |
| self._verifyRunResults(expectedResults, max_time=12.0) |
| |
| def testParallelStageCommunicationNotQueueable(self): |
| """Test setting non-queueable run attr in parallel stage.""" |
| bs = FakeBuildStore() |
| stage_objs = [ |
| SetAttrStage(self._run, bs, attr='release_tag'), |
| GetAttrStage(self._run, bs, timeout=2), |
| ] |
| error = self._TestParallelStages(stage_objs) |
| self.assertTrue(error) |
| expectedResults = [ |
| ('SetAttr', stage_objs[0].QueueableException()), |
| ('GetAttr', stage_objs[1].TimeoutException()), |
| ] |
| self._verifyRunResults(expectedResults, max_time=12.0) |
| |
| def testStagesReportSuccess(self): |
| """Tests Stage reporting.""" |
| |
| # Store off a known set of results and generate a report |
| results_lib.Results.Record('Sync', results_lib.Results.SUCCESS, time=1) |
| results_lib.Results.Record('Build', results_lib.Results.SUCCESS, time=2) |
| results_lib.Results.Record('Test', FailStage.FAIL_EXCEPTION, time=3) |
| results_lib.Results.Record('SignerTests', results_lib.Results.SKIPPED) |
| result = cros_build_lib.CommandResult( |
| cmd=['/bin/false', '/nosuchdir'], returncode=2) |
| results_lib.Results.Record( |
| 'Archive', |
| cros_build_lib.RunCommandError( |
| 'Command "/bin/false /nosuchdir" failed.\n', result), |
| time=4) |
| |
| results = StringIO() |
| |
| results_lib.Results.Report(results) |
| |
| expectedResults = ( |
| '************************************************************\n' |
| '** Stage Results\n' |
| '************************************************************\n' |
| '** PASS Sync (0:00:01)\n' |
| '************************************************************\n' |
| '** PASS Build (0:00:02)\n' |
| '************************************************************\n' |
| '** FAIL Test (0:00:03) with StepFailure\n' |
| '************************************************************\n' |
| '** FAIL Archive (0:00:04) in /bin/false\n' |
| '************************************************************\n') |
| |
| expectedLines = expectedResults.split('\n') |
| actualLines = results.getvalue().split('\n') |
| |
| # Break out the asserts to be per item to make debugging easier |
| for i in range(min(len(actualLines), len(expectedLines))): |
| self.assertEqual(expectedLines[i], actualLines[i]) |
| self.assertEqual(len(expectedLines), len(actualLines)) |
| |
| def testStagesReportError(self): |
| """Tests Stage reporting with exceptions.""" |
| |
| # Store off a known set of results and generate a report |
| results_lib.Results.Record('Sync', results_lib.Results.SUCCESS, time=1) |
| results_lib.Results.Record('Build', results_lib.Results.SUCCESS, time=2) |
| results_lib.Results.Record( |
| 'Test', FailStage.FAIL_EXCEPTION, 'failException Msg\nLine 2', time=3) |
| result = cros_build_lib.CommandResult( |
| cmd=['/bin/false', '/nosuchdir'], returncode=2) |
| results_lib.Results.Record( |
| 'Archive', |
| cros_build_lib.RunCommandError( |
| 'Command "/bin/false /nosuchdir" failed.\n', result), |
| 'FailRunCommand msg', |
| time=4) |
| |
| results = StringIO() |
| |
| results_lib.Results.Report(results) |
| |
| expectedResults = ( |
| '************************************************************\n' |
| '** Stage Results\n' |
| '************************************************************\n' |
| '** PASS Sync (0:00:01)\n' |
| '************************************************************\n' |
| '** PASS Build (0:00:02)\n' |
| '************************************************************\n' |
| '** FAIL Test (0:00:03) with StepFailure\n' |
| '************************************************************\n' |
| '** FAIL Archive (0:00:04) in /bin/false\n' |
| '************************************************************\n' |
| '\n' |
| 'Failed in stage Test:\n' |
| '\n' |
| 'failException Msg\n' |
| 'Line 2\n' |
| '\n' |
| 'Failed in stage Archive:\n' |
| '\n' |
| 'FailRunCommand msg\n') |
| |
| expectedLines = expectedResults.split('\n') |
| actualLines = results.getvalue().split('\n') |
| |
| # Break out the asserts to be per item to make debugging easier |
| for i in range(min(len(actualLines), len(expectedLines))): |
| self.assertEqual(expectedLines[i], actualLines[i]) |
| self.assertEqual(len(expectedLines), len(actualLines)) |
| |
| def testStagesReportReleaseTag(self): |
| """Tests Release Tag entry in stages report.""" |
| |
| current_version = 'release_tag_string' |
| # Store off a known set of results and generate a report |
| results_lib.Results.Record('Pass', results_lib.Results.SUCCESS, time=1) |
| |
| results = StringIO() |
| |
| results_lib.Results.Report(results, current_version) |
| |
| expectedResults = ( |
| '************************************************************\n' |
| '** RELEASE VERSION: release_tag_string\n' |
| '************************************************************\n' |
| '** Stage Results\n' |
| '************************************************************\n' |
| '** PASS Pass (0:00:01)\n' |
| '************************************************************\n') |
| |
| expectedLines = expectedResults.split('\n') |
| actualLines = results.getvalue().split('\n') |
| |
| # Break out the asserts to be per item to make debugging easier |
| for expectedLine, actualLine in zip(expectedLines, actualLines): |
| self.assertEqual(expectedLine, actualLine) |
| self.assertEqual(len(expectedLines), len(actualLines)) |
| |
| def testSaveCompletedStages(self): |
| """Tests that we can save out completed stages.""" |
| |
| # Run this again to make sure we have the expected results stored |
| results_lib.Results.Record('Pass', results_lib.Results.SUCCESS) |
| results_lib.Results.Record('Fail', FailStage.FAIL_EXCEPTION) |
| results_lib.Results.Record('Pass2', results_lib.Results.SUCCESS) |
| |
| saveFile = StringIO() |
| results_lib.Results.SaveCompletedStages(saveFile) |
| self.assertEqual(saveFile.getvalue(), self._PassString()) |
| |
| def testRestoreCompletedStages(self): |
| """Tests that we can read in completed stages.""" |
| |
| results_lib.Results.RestoreCompletedStages( |
| StringIO(self._PassString())) |
| |
| previous = results_lib.Results.GetPrevious() |
| self.assertEqual(list(previous), ['Pass']) |
| |
| def testRunAfterRestore(self): |
| """Tests that we skip previously completed stages.""" |
| |
| # Fake results_lib.Results.RestoreCompletedStages |
| results_lib.Results.RestoreCompletedStages( |
| StringIO(self._PassString())) |
| |
| self._runStages() |
| |
| # Verify that the results are what we expect. |
| expectedResults = [ |
| ('Pass', results_lib.Results.SUCCESS), |
| ('Pass2', results_lib.Results.SUCCESS), |
| ('Fail', FailStage.FAIL_EXCEPTION), |
| ] |
| self._verifyRunResults(expectedResults) |
| |
| def testFailedButForgiven(self): |
| """Tests that warnings are flagged as such.""" |
| results_lib.Results.Record('Warn', results_lib.Results.FORGIVEN, time=1) |
| results = StringIO() |
| results_lib.Results.Report(results) |
| self.assertTrue('@@@STEP_WARNINGS@@@' in results.getvalue()) |