| #!/usr/bin/env vpython3 |
| # Copyright 2020 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from typing import Iterable, Optional |
| import unittest |
| from unittest import mock |
| |
| # //testing imports. |
| from unexpected_passes_common import builders |
| from unexpected_passes_common import constants |
| from unexpected_passes_common import data_types |
| from unexpected_passes_common import expectations |
| from unexpected_passes_common import queries |
| from unexpected_passes_common import unittest_utils as uu |
| |
| # Protected access is allowed for unittests. |
| # pylint: disable=protected-access |
| |
| class HelperMethodUnittest(unittest.TestCase): |
| def testStripPrefixFromBuildIdValidId(self) -> None: |
| self.assertEqual(queries._StripPrefixFromBuildId('build-1'), '1') |
| |
| def testStripPrefixFromBuildIdInvalidId(self) -> None: |
| with self.assertRaises(AssertionError): |
| queries._StripPrefixFromBuildId('build1') |
| with self.assertRaises(AssertionError): |
| queries._StripPrefixFromBuildId('build-1-2') |
| |
| def testConvertActualResultToExpectationFileFormatAbort(self) -> None: |
| self.assertEqual( |
| queries._ConvertActualResultToExpectationFileFormat('ABORT'), 'Timeout') |
| |
| |
| class BigQueryQuerierInitUnittest(unittest.TestCase): |
| |
| def testInvalidNumSamples(self): |
| """Tests that the number of samples is validated.""" |
| with self.assertRaises(AssertionError): |
| uu.CreateGenericQuerier(num_samples=-1) |
| |
| def testDefaultSamples(self): |
| """Tests that the number of samples is set to a default if not provided.""" |
| querier = uu.CreateGenericQuerier(num_samples=0) |
| self.assertGreater(querier._num_samples, 0) |
| |
| |
| class GetBuilderGroupedQueryResultsUnittest(unittest.TestCase): |
| |
| def setUp(self): |
| builders.ClearInstance() |
| expectations.ClearInstance() |
| uu.RegisterGenericBuildersImplementation() |
| uu.RegisterGenericExpectationsImplementation() |
| self._querier = uu.CreateGenericQuerier() |
| |
| def testUnknownBuilderType(self): |
| """Tests behavior when an unknown builder type is provided.""" |
| with self.assertRaisesRegex(RuntimeError, 'Unknown builder type unknown'): |
| for _ in self._querier.GetBuilderGroupedQueryResults('unknown', False): |
| pass |
| |
| def testQueryRouting(self): |
| """Tests that the correct query is used based on inputs.""" |
| with mock.patch.object(self._querier, |
| '_GetPublicCiQuery', |
| return_value='public_ci') as public_ci_mock: |
| with mock.patch.object(self._querier, |
| '_GetInternalCiQuery', |
| return_value='internal_ci') as internal_ci_mock: |
| with mock.patch.object(self._querier, |
| '_GetPublicTryQuery', |
| return_value='public_try') as public_try_mock: |
| with mock.patch.object( |
| self._querier, |
| '_GetInternalTryQuery', |
| return_value='internal_try') as internal_try_mock: |
| all_mocks = [ |
| public_ci_mock, |
| internal_ci_mock, |
| public_try_mock, |
| internal_try_mock, |
| ] |
| inputs = [ |
| (constants.BuilderTypes.CI, False, public_ci_mock), |
| (constants.BuilderTypes.CI, True, internal_ci_mock), |
| (constants.BuilderTypes.TRY, False, public_try_mock), |
| (constants.BuilderTypes.TRY, True, internal_try_mock), |
| ] |
| for builder_type, internal_status, called_mock in inputs: |
| for _ in self._querier.GetBuilderGroupedQueryResults( |
| builder_type, internal_status): |
| pass |
| for m in all_mocks: |
| if m == called_mock: |
| m.assert_called_once() |
| else: |
| m.assert_not_called() |
| for m in all_mocks: |
| m.reset_mock() |
| |
| def testNoResults(self): |
| """Tests functionality if the query returns no results.""" |
| returned_builders = [] |
| with self.assertLogs(level='WARNING') as log_manager: |
| with mock.patch.object(self._querier, |
| '_GetPublicCiQuery', |
| return_value=''): |
| for builder_name, _, _ in self._querier.GetBuilderGroupedQueryResults( |
| constants.BuilderTypes.CI, False): |
| returned_builders.append(builder_name) |
| for message in log_manager.output: |
| if ('Did not get any results for builder type ci and internal status ' |
| 'False. Depending on where tests are run and how frequently ' |
| 'trybots are used for submission, this may be benign') in message: |
| break |
| else: |
| self.fail('Did not find expected log message: %s' % log_manager.output) |
| self.assertEqual(len(returned_builders), 0) |
| |
| def testHappyPath(self): |
| """Tests functionality in the happy path.""" |
| self._querier.query_results = [ |
| uu.FakeQueryResult(builder_name='builder_a', |
| id_='build-a', |
| test_id='test_a', |
| status='PASS', |
| typ_tags=['linux', 'unknown_tag'], |
| step_name='step_a'), |
| uu.FakeQueryResult(builder_name='builder_b', |
| id_='build-b', |
| test_id='test_b', |
| status='FAIL', |
| typ_tags=['win'], |
| step_name='step_b'), |
| ] |
| |
| expected_results = [ |
| ('builder_a', |
| [data_types.BaseResult('test_a', ('linux', ), 'Pass', 'step_a', |
| 'a')], None), |
| ('builder_b', |
| [data_types.BaseResult('test_b', ('win', ), 'Failure', 'step_b', |
| 'b')], None), |
| ] |
| |
| results = [] |
| with mock.patch.object(self._querier, '_GetPublicCiQuery', return_value=''): |
| for builder_name, result_list, expectation_files in ( |
| self._querier.GetBuilderGroupedQueryResults(constants.BuilderTypes.CI, |
| False)): |
| results.append((builder_name, result_list, expectation_files)) |
| |
| self.assertEqual(results, expected_results) |
| |
| def testHappyPathWithExpectationFiles(self): |
| """Tests functionality in the happy path with expectation files provided.""" |
| self._querier.query_results = [ |
| uu.FakeQueryResult(builder_name='builder_a', |
| id_='build-a', |
| test_id='test_a', |
| status='PASS', |
| typ_tags=['linux', 'unknown_tag'], |
| step_name='step_a'), |
| uu.FakeQueryResult(builder_name='builder_b', |
| id_='build-b', |
| test_id='test_b', |
| status='FAIL', |
| typ_tags=['win'], |
| step_name='step_b'), |
| ] |
| |
| expected_results = [ |
| ('builder_a', |
| [data_types.BaseResult('test_a', ('linux', ), 'Pass', 'step_a', |
| 'a')], list(set(['ef_a']))), |
| ('builder_b', |
| [data_types.BaseResult('test_b', ('win', ), 'Failure', 'step_b', |
| 'b')], list(set(['ef_b', 'ef_c']))), |
| ] |
| |
| results = [] |
| with mock.patch.object(self._querier, |
| '_GetRelevantExpectationFilesForQueryResult', |
| side_effect=(['ef_a'], ['ef_b', 'ef_c'])): |
| with mock.patch.object(self._querier, |
| '_GetPublicCiQuery', |
| return_value=''): |
| for builder_name, result_list, expectation_files in ( |
| self._querier.GetBuilderGroupedQueryResults( |
| constants.BuilderTypes.CI, False)): |
| results.append((builder_name, result_list, expectation_files)) |
| |
| self.assertEqual(results, expected_results) |
| |
| |
| class FillExpectationMapForBuildersUnittest(unittest.TestCase): |
| def setUp(self) -> None: |
| self._querier = uu.CreateGenericQuerier() |
| |
| expectations.ClearInstance() |
| uu.RegisterGenericExpectationsImplementation() |
| |
| def testErrorOnMixedBuilders(self) -> None: |
| """Tests that providing builders of mixed type is an error.""" |
| builders_to_fill = [ |
| data_types.BuilderEntry('ci_builder', constants.BuilderTypes.CI, False), |
| data_types.BuilderEntry('try_builder', constants.BuilderTypes.TRY, |
| False) |
| ] |
| with self.assertRaises(AssertionError): |
| self._querier.FillExpectationMapForBuilders( |
| data_types.TestExpectationMap({}), builders_to_fill) |
| |
| def _runValidResultsTest(self, keep_unmatched_results: bool) -> None: |
| self._querier = uu.CreateGenericQuerier( |
| keep_unmatched_results=keep_unmatched_results) |
| |
| public_results = [ |
| uu.FakeQueryResult(builder_name='matched_builder', |
| id_='build-build_id', |
| test_id='foo', |
| status='PASS', |
| typ_tags=['win'], |
| step_name='step_name'), |
| uu.FakeQueryResult(builder_name='unmatched_builder', |
| id_='build-build_id', |
| test_id='bar', |
| status='PASS', |
| typ_tags=[], |
| step_name='step_name'), |
| uu.FakeQueryResult(builder_name='extra_builder', |
| id_='build-build_id', |
| test_id='foo', |
| status='PASS', |
| typ_tags=['win'], |
| step_name='step_name'), |
| ] |
| |
| internal_results = [ |
| uu.FakeQueryResult(builder_name='matched_internal', |
| id_='build-build_id', |
| test_id='foo', |
| status='PASS', |
| typ_tags=['win'], |
| step_name='step_name_internal'), |
| uu.FakeQueryResult(builder_name='unmatched_internal', |
| id_='build-build_id', |
| test_id='bar', |
| status='PASS', |
| typ_tags=[], |
| step_name='step_name_internal'), |
| ] |
| |
| builders_to_fill = [ |
| data_types.BuilderEntry('matched_builder', constants.BuilderTypes.CI, |
| False), |
| data_types.BuilderEntry('unmatched_builder', constants.BuilderTypes.CI, |
| False), |
| data_types.BuilderEntry('matched_internal', constants.BuilderTypes.CI, |
| True), |
| data_types.BuilderEntry('unmatched_internal', constants.BuilderTypes.CI, |
| True), |
| ] |
| |
| expectation = data_types.Expectation('foo', ['win'], 'RetryOnFailure', |
| data_types.WildcardType.NON_WILDCARD) |
| expectation_map = data_types.TestExpectationMap({ |
| 'foo': |
| data_types.ExpectationBuilderMap({ |
| expectation: |
| data_types.BuilderStepMap(), |
| }), |
| }) |
| |
| def PublicSideEffect(): |
| self._querier.query_results = public_results |
| return '' |
| |
| def InternalSideEffect(): |
| self._querier.query_results = internal_results |
| return '' |
| |
| with self.assertLogs(level='WARNING') as log_manager: |
| with mock.patch.object(self._querier, |
| '_GetPublicCiQuery', |
| side_effect=PublicSideEffect) as public_mock: |
| with mock.patch.object(self._querier, |
| '_GetInternalCiQuery', |
| side_effect=InternalSideEffect) as internal_mock: |
| unmatched_results = self._querier.FillExpectationMapForBuilders( |
| expectation_map, builders_to_fill) |
| public_mock.assert_called_once() |
| internal_mock.assert_called_once() |
| |
| for message in log_manager.output: |
| if ('Did not find a matching builder for name extra_builder and ' |
| 'internal status False. This is normal if the builder is no longer ' |
| 'running tests (e.g. it was experimental).') in message: |
| break |
| else: |
| self.fail('Did not find expected log message') |
| |
| stats = data_types.BuildStats() |
| stats.AddPassedBuild(frozenset(['win'])) |
| expected_expectation_map = { |
| 'foo': { |
| expectation: { |
| 'chromium/ci:matched_builder': { |
| 'step_name': stats, |
| }, |
| 'chrome/ci:matched_internal': { |
| 'step_name_internal': stats, |
| }, |
| }, |
| }, |
| } |
| self.assertEqual(expectation_map, expected_expectation_map) |
| if keep_unmatched_results: |
| self.assertEqual( |
| unmatched_results, { |
| 'chromium/ci:unmatched_builder': [ |
| data_types.Result('bar', [], 'Pass', 'step_name', 'build_id'), |
| ], |
| 'chrome/ci:unmatched_internal': [ |
| data_types.Result('bar', [], 'Pass', 'step_name_internal', |
| 'build_id'), |
| ], |
| }) |
| else: |
| self.assertEqual(unmatched_results, {}) |
| |
| def testValidResultsKeepUnmatched(self) -> None: |
| """Tests behavior w/ valid results and keeping unmatched results.""" |
| self._runValidResultsTest(True) |
| |
| def testValidResultsDoNotKeepUnmatched(self) -> None: |
| """Tests behavior w/ valid results and not keeping unmatched results.""" |
| self._runValidResultsTest(False) |
| |
| |
| class ProcessRowsForBuilderUnittest(unittest.TestCase): |
| |
| def setUp(self): |
| self._querier = uu.CreateGenericQuerier() |
| |
| def testHappyPathWithExpectationFiles(self): |
| """Tests functionality along the happy path with expectation files.""" |
| |
| def SideEffect(row: queries.QueryResult) -> Optional[Iterable[str]]: |
| if row.step_name == 'step_a1': |
| return ['ef_a1'] |
| if row.step_name == 'step_a2': |
| return ['ef_a2'] |
| if row.step_name == 'step_b': |
| return ['ef_b1', 'ef_b2'] |
| raise RuntimeError('Unexpected row') |
| |
| rows = [ |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-a', |
| test_id='test_a', |
| status='PASS', |
| typ_tags=['linux', 'unknown_tag'], |
| step_name='step_a1'), |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-a', |
| test_id='test_a', |
| status='FAIL', |
| typ_tags=['linux', 'unknown_tag'], |
| step_name='step_a2'), |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-b', |
| test_id='test_b', |
| status='FAIL', |
| typ_tags=['win'], |
| step_name='step_b'), |
| ] |
| |
| # Reversed order is expected since results are popped. |
| expected_results = [ |
| data_types.BaseResult(test='test_b', |
| tags=['win'], |
| actual_result='Failure', |
| step='step_b', |
| build_id='b'), |
| data_types.BaseResult(test='test_a', |
| tags=['linux'], |
| actual_result='Failure', |
| step='step_a2', |
| build_id='a'), |
| data_types.BaseResult(test='test_a', |
| tags=['linux'], |
| actual_result='Pass', |
| step='step_a1', |
| build_id='a'), |
| ] |
| |
| with mock.patch.object(self._querier, |
| '_GetRelevantExpectationFilesForQueryResult', |
| side_effect=SideEffect): |
| results, expectation_files = self._querier._ProcessRowsForBuilder(rows) |
| self.assertEqual(results, expected_results) |
| self.assertEqual(len(expectation_files), len(set(expectation_files))) |
| self.assertEqual(set(expectation_files), |
| set(['ef_a1', 'ef_a2', 'ef_b1', 'ef_b2'])) |
| |
| def testHappyPathNoneExpectation(self): |
| """Tests functionality along the happy path with a None expectation file.""" |
| |
| # A single None expectation file should cause the resulting return value to |
| # become None. |
| def SideEffect(row: queries.QueryResult) -> Optional[Iterable[str]]: |
| if row.step_name == 'step_a1': |
| return ['ef_a1'] |
| if row.step_name == 'step_a2': |
| return ['ef_a2'] |
| return None |
| |
| rows = [ |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-a', |
| test_id='test_a', |
| status='PASS', |
| typ_tags=['linux', 'unknown_tag'], |
| step_name='step_a1'), |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-a', |
| test_id='test_a', |
| status='FAIL', |
| typ_tags=['linux', 'unknown_tag'], |
| step_name='step_a2'), |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-b', |
| test_id='test_b', |
| status='FAIL', |
| typ_tags=['win'], |
| step_name='step_b'), |
| ] |
| |
| # Reversed order is expected since results are popped. |
| expected_results = [ |
| data_types.BaseResult(test='test_b', |
| tags=['win'], |
| actual_result='Failure', |
| step='step_b', |
| build_id='b'), |
| data_types.BaseResult(test='test_a', |
| tags=['linux'], |
| actual_result='Failure', |
| step='step_a2', |
| build_id='a'), |
| data_types.BaseResult(test='test_a', |
| tags=['linux'], |
| actual_result='Pass', |
| step='step_a1', |
| build_id='a'), |
| ] |
| |
| with mock.patch.object(self._querier, |
| '_GetRelevantExpectationFilesForQueryResult', |
| side_effect=SideEffect): |
| results, expectation_files = self._querier._ProcessRowsForBuilder(rows) |
| self.assertEqual(results, expected_results) |
| self.assertEqual(expectation_files, None) |
| |
| def testHappyPathSkippedResult(self): |
| """Tests functionality along the happy path with a skipped result.""" |
| |
| def SideEffect(row: queries.QueryResult) -> bool: |
| if row.step_name == 'step_b': |
| return True |
| return False |
| |
| rows = [ |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-a', |
| test_id='test_a', |
| status='PASS', |
| typ_tags=['linux', 'unknown_tag'], |
| step_name='step_a1'), |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-a', |
| test_id='test_a', |
| status='FAIL', |
| typ_tags=['linux', 'unknown_tag'], |
| step_name='step_a2'), |
| uu.FakeQueryResult(builder_name='unused', |
| id_='build-b', |
| test_id='test_b', |
| status='FAIL', |
| typ_tags=['win'], |
| step_name='step_b'), |
| ] |
| |
| # Reversed order is expected since results are popped. |
| expected_results = [ |
| data_types.BaseResult(test='test_a', |
| tags=['linux'], |
| actual_result='Failure', |
| step='step_a2', |
| build_id='a'), |
| data_types.BaseResult(test='test_a', |
| tags=['linux'], |
| actual_result='Pass', |
| step='step_a1', |
| build_id='a'), |
| ] |
| |
| with mock.patch.object(self._querier, |
| '_ShouldSkipOverResult', |
| side_effect=SideEffect): |
| results, expectation_files = self._querier._ProcessRowsForBuilder(rows) |
| self.assertEqual(results, expected_results) |
| self.assertEqual(expectation_files, None) |
| |
| |
| if __name__ == '__main__': |
| unittest.main(verbosity=2) |