| # Copyright 2016 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import json |
| import optparse |
| import textwrap |
| |
| from blinkpy.common.checkout.git_mock import MockGit |
| from blinkpy.common.net.buildbot import Build |
| from blinkpy.common.net.git_cl import TryJobStatus |
| from blinkpy.common.net.git_cl_mock import MockGitCL |
| from blinkpy.common.net.web_test_results import WebTestResults |
| from blinkpy.common.path_finder import RELATIVE_WEB_TESTS |
| from blinkpy.common.system.log_testing import LoggingTestCase |
| from blinkpy.tool.commands.rebaseline import TestBaselineSet |
| from blinkpy.tool.commands.rebaseline_cl import RebaselineCL |
| from blinkpy.tool.commands.rebaseline_unittest import BaseTestCase |
| from blinkpy.web_tests.builder_list import BuilderList |
| |
| |
| class RebaselineCLTest(BaseTestCase, LoggingTestCase): |
| |
| command_constructor = RebaselineCL |
| |
| def setUp(self): |
| BaseTestCase.setUp(self) |
| LoggingTestCase.setUp(self) |
| |
| builds = { |
| Build('MOCK Try Win', 5000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Mac', 4000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Linux', 6000): TryJobStatus('COMPLETED', 'FAILURE'), |
| } |
| |
| self.command.git_cl = MockGitCL(self.tool, builds) |
| |
| git = MockGit(filesystem=self.tool.filesystem, executive=self.tool.executive) |
| git.changed_files = lambda **_: [ |
| RELATIVE_WEB_TESTS + 'one/text-fail.html', |
| RELATIVE_WEB_TESTS + 'one/flaky-fail.html', |
| ] |
| self.tool.git = lambda: git |
| |
| self.tool.builders = BuilderList({ |
| 'MOCK Try Win': { |
| 'port_name': 'test-win-win7', |
| 'specifiers': ['Win7', 'Release'], |
| 'is_try_builder': True, |
| }, |
| 'MOCK Try Linux': { |
| 'port_name': 'test-linux-trusty', |
| 'specifiers': ['Trusty', 'Release'], |
| 'is_try_builder': True, |
| }, |
| 'MOCK Try Mac': { |
| 'port_name': 'test-mac-mac10.11', |
| 'specifiers': ['Mac10.11', 'Release'], |
| 'is_try_builder': True, |
| }, |
| }) |
| web_test_results = WebTestResults({ |
| 'tests': { |
| 'one': { |
| 'crash.html': {'expected': 'PASS', 'actual': 'CRASH', 'is_unexpected': True}, |
| 'expected-fail.html': {'expected': 'FAIL', 'actual': 'IMAGE+TEXT'}, |
| 'flaky-fail.html': {'expected': 'PASS', 'actual': 'PASS TEXT', 'is_unexpected': True}, |
| 'missing.html': {'expected': 'PASS', 'actual': 'MISSING', 'is_unexpected': True}, |
| 'slow-fail.html': {'expected': 'SLOW', 'actual': 'TEXT', 'is_unexpected': True}, |
| 'text-fail.html': {'expected': 'PASS', 'actual': 'TEXT', 'is_unexpected': True}, |
| 'unexpected-pass.html': {'expected': 'FAIL', 'actual': 'PASS', 'is_unexpected': True}, |
| }, |
| 'two': {'image-fail.html': {'expected': 'PASS', 'actual': 'IMAGE', 'is_unexpected': True}}, |
| }, |
| }) |
| |
| for build in builds: |
| self.tool.buildbot.set_results(build, web_test_results) |
| self.tool.buildbot.set_retry_sumary_json(build, json.dumps({ |
| 'failures': [ |
| 'one/flaky-fail.html', |
| 'one/missing.html', |
| 'one/slow-fail.html', |
| 'one/text-fail.html', |
| 'two/image-fail.html', |
| ], |
| 'ignored': [], |
| })) |
| |
| # Write to the mock filesystem so that these tests are considered to exist. |
| tests = [ |
| 'one/flaky-fail.html', |
| 'one/missing.html', |
| 'one/slow-fail.html', |
| 'one/text-fail.html', |
| 'two/image-fail.html', |
| ] |
| for test in tests: |
| path = self.mac_port.host.filesystem.join( |
| self.mac_port.web_tests_dir(), test) |
| self._write(path, 'contents') |
| |
| self.mac_port.host.filesystem.write_text_file( |
| '/test.checkout/web_tests/external/wpt/MANIFEST.json', '{}') |
| |
| def tearDown(self): |
| BaseTestCase.tearDown(self) |
| LoggingTestCase.tearDown(self) |
| |
| @staticmethod |
| def command_options(**kwargs): |
| options = { |
| 'dry_run': False, |
| 'only_changed_tests': False, |
| 'trigger_jobs': True, |
| 'fill_missing': None, |
| 'optimize': True, |
| 'results_directory': None, |
| 'test_name_file': None, |
| 'verbose': False, |
| 'builders': [], |
| 'patchset': None, |
| } |
| options.update(kwargs) |
| return optparse.Values(dict(**options)) |
| |
| def test_execute_basic(self): |
| # By default, with no arguments or options, rebaseline-cl rebaselines |
| # all of the tests that unexpectedly failed. |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 0) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: Rebaselining one/flaky-fail.html\n', |
| 'INFO: Rebaselining one/missing.html\n', |
| 'INFO: Rebaselining one/slow-fail.html\n', |
| 'INFO: Rebaselining one/text-fail.html\n', |
| 'INFO: Rebaselining two/image-fail.html\n', |
| ]) |
| |
| def test_execute_with_test_name_file(self): |
| fs = self.mac_port.host.filesystem |
| test_name_file = fs.mktemp() |
| fs.write_text_file(test_name_file, textwrap.dedent(''' |
| one/flaky-fail.html |
| one/missing.html |
| # one/slow-fail.html |
| # |
| |
| one/text-fail.html |
| two/image-fail.html ''')) |
| exit_code = self.command.execute( |
| self.command_options(test_name_file=test_name_file), [], self.tool) |
| self.assertEqual(exit_code, 0) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: Reading list of tests to rebaseline from %s\n' % test_name_file, |
| 'INFO: Rebaselining one/flaky-fail.html\n', |
| 'INFO: Rebaselining one/missing.html\n', |
| 'INFO: Rebaselining one/text-fail.html\n', |
| 'INFO: Rebaselining two/image-fail.html\n', |
| ]) |
| |
| def test_execute_with_no_issue_number_aborts(self): |
| # If the user hasn't uploaded a CL, an error message is printed. |
| self.command.git_cl = MockGitCL(self.tool, issue_number='None') |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog(['ERROR: No issue number for current branch.\n']) |
| |
| def test_execute_with_unstaged_baselines_aborts(self): |
| git = self.tool.git() |
| git.unstaged_changes = lambda: { |
| RELATIVE_WEB_TESTS + 'my-test-expected.txt': '?' |
| } |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog([ |
| 'ERROR: Aborting: there are unstaged baselines:\n', |
| 'ERROR: /mock-checkout/' + RELATIVE_WEB_TESTS + |
| 'my-test-expected.txt\n', |
| ]) |
| |
| def test_execute_no_try_jobs_started_triggers_jobs(self): |
| # If there are no try jobs started yet, by default the tool will |
| # trigger new try jobs. |
| self.command.git_cl = MockGitCL(self.tool, {}) |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog([ |
| 'INFO: No finished try jobs.\n', |
| 'INFO: Triggering try jobs:\n', |
| 'INFO: MOCK Try Linux\n', |
| 'INFO: MOCK Try Mac\n', |
| 'INFO: MOCK Try Win\n', |
| 'INFO: Once all pending try jobs have finished, please re-run\n' |
| 'blink_tool.py rebaseline-cl to fetch new baselines.\n' |
| ]) |
| |
| def test_execute_no_try_jobs_started_and_no_trigger_jobs(self): |
| # If there are no try jobs started yet and --no-trigger-jobs is passed, |
| # then we just abort immediately. |
| self.command.git_cl = MockGitCL(self.tool, {}) |
| exit_code = self.command.execute( |
| self.command_options(trigger_jobs=False), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog([ |
| 'INFO: No finished try jobs.\n', |
| 'INFO: Aborted: no try jobs and --no-trigger-jobs passed.\n', |
| ]) |
| |
| def test_execute_one_missing_build(self): |
| builds = { |
| Build('MOCK Try Win', 5000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Mac', 4000): TryJobStatus('COMPLETED', 'FAILURE'), |
| } |
| self.command.git_cl = MockGitCL(self.tool, builds) |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog([ |
| 'INFO: Finished try jobs:\n', |
| 'INFO: MOCK Try Mac\n', |
| 'INFO: MOCK Try Win\n', |
| 'INFO: Triggering try jobs:\n', |
| 'INFO: MOCK Try Linux\n', |
| 'INFO: Once all pending try jobs have finished, please re-run\n' |
| 'blink_tool.py rebaseline-cl to fetch new baselines.\n', |
| ]) |
| |
| def test_execute_with_unfinished_jobs(self): |
| builds = { |
| Build('MOCK Try Win', 5000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Mac', 4000): TryJobStatus('STARTED'), |
| Build('MOCK Try Linux', 6000): TryJobStatus('SCHEDULED'), |
| } |
| self.command.git_cl = MockGitCL(self.tool, builds) |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog([ |
| 'INFO: Finished try jobs:\n', |
| 'INFO: MOCK Try Win\n', |
| 'INFO: Scheduled or started try jobs:\n', |
| 'INFO: MOCK Try Linux\n', |
| 'INFO: MOCK Try Mac\n', |
| 'INFO: There are some builders with no results:\n', |
| 'INFO: MOCK Try Linux\n', |
| 'INFO: MOCK Try Mac\n', |
| 'INFO: Would you like to continue?\n', |
| 'INFO: Aborting.\n', |
| ]) |
| |
| def test_execute_with_canceled_job(self): |
| builds = { |
| Build('MOCK Try Win', 5000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Mac', 4000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Linux', 6000): TryJobStatus('COMPLETED', 'CANCELED'), |
| } |
| self.command.git_cl = MockGitCL(self.tool, builds) |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: There are some builders with no results:\n', |
| 'INFO: MOCK Try Linux\n', |
| 'INFO: Would you like to continue?\n', |
| 'INFO: Aborting.\n', |
| ]) |
| |
| def test_execute_with_passing_jobs(self): |
| builds = { |
| Build('MOCK Try Win', 5000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Mac', 4000): TryJobStatus('COMPLETED', 'SUCCESS'), |
| Build('MOCK Try Linux', 6000): TryJobStatus('COMPLETED', 'SUCCESS'), |
| } |
| self.command.git_cl = MockGitCL(self.tool, builds) |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 0) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: Rebaselining one/flaky-fail.html\n', |
| 'INFO: Rebaselining one/missing.html\n', |
| 'INFO: Rebaselining one/slow-fail.html\n', |
| 'INFO: Rebaselining one/text-fail.html\n', |
| 'INFO: Rebaselining two/image-fail.html\n' |
| ]) |
| |
| def test_execute_with_no_trigger_jobs_option(self): |
| builds = { |
| Build('MOCK Try Win', 5000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Mac', 4000): TryJobStatus('COMPLETED', 'FAILURE'), |
| } |
| self.command.git_cl = MockGitCL(self.tool, builds) |
| exit_code = self.command.execute( |
| self.command_options(trigger_jobs=False), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog([ |
| 'INFO: Finished try jobs:\n', |
| 'INFO: MOCK Try Mac\n', |
| 'INFO: MOCK Try Win\n', |
| 'INFO: There are some builders with no results:\n', |
| 'INFO: MOCK Try Linux\n', |
| 'INFO: Would you like to continue?\n', |
| 'INFO: Aborting.\n', |
| ]) |
| |
| def test_execute_with_only_changed_tests_option(self): |
| # When --only-changed-tests is passed, the tool only rebaselines tests |
| # that were modified in the CL. |
| exit_code = self.command.execute( |
| self.command_options(only_changed_tests=True), [], self.tool) |
| self.assertEqual(exit_code, 0) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: Rebaselining one/flaky-fail.html\n', |
| 'INFO: Rebaselining one/text-fail.html\n', |
| ]) |
| |
| def test_execute_with_test_that_fails_on_retry(self): |
| # In this example, one test failed both with and without the patch |
| # in the try job, so it is not rebaselined. |
| builds = { |
| Build('MOCK Try Win', 5000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Mac', 4000): TryJobStatus('COMPLETED', 'FAILURE'), |
| Build('MOCK Try Linux', 6000): TryJobStatus('COMPLETED', 'FAILURE'), |
| } |
| for build in builds: |
| self.tool.buildbot.set_retry_sumary_json(build, json.dumps({ |
| 'failures': ['one/text-fail.html'], |
| 'ignored': ['two/image-fail.html'], |
| })) |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 0) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: Rebaselining one/text-fail.html\n', |
| ]) |
| |
| def test_execute_with_no_retry_summary_downloaded(self): |
| # In this example, the retry summary could not be downloaded, so |
| # a warning is printed and all tests are rebaselined. |
| self.tool.buildbot.set_retry_sumary_json( |
| Build('MOCK Try Win', 5000), None) |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 0) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'WARNING: No retry summary available for "MOCK Try Win".\n', |
| 'INFO: Rebaselining one/flaky-fail.html\n', |
| 'INFO: Rebaselining one/missing.html\n', |
| 'INFO: Rebaselining one/slow-fail.html\n', |
| 'INFO: Rebaselining one/text-fail.html\n', |
| 'INFO: Rebaselining two/image-fail.html\n', |
| ]) |
| |
| def test_rebaseline_command_invocations(self): |
| """Tests the list of commands that are called for rebaselining.""" |
| # First write test contents to the mock filesystem so that |
| # one/flaky-fail.html is considered a real test to rebaseline. |
| port = self.tool.port_factory.get('test-win-win7') |
| path = port.host.filesystem.join( |
| port.web_tests_dir(), 'one/flaky-fail.html') |
| self._write(path, 'contents') |
| test_baseline_set = TestBaselineSet(self.tool) |
| test_baseline_set.add( |
| 'one/flaky-fail.html', Build('MOCK Try Win', 5000)) |
| self.command.rebaseline(self.command_options(), test_baseline_set) |
| self.assertEqual( |
| self.tool.executive.calls, |
| [ |
| [[ |
| 'python', 'echo', 'copy-existing-baselines-internal', |
| '--test', 'one/flaky-fail.html', |
| '--suffixes', 'txt', |
| '--port-name', 'test-win-win7', |
| ]], |
| [[ |
| 'python', 'echo', 'rebaseline-test-internal', |
| '--test', 'one/flaky-fail.html', |
| '--suffixes', 'txt', |
| '--port-name', 'test-win-win7', |
| '--builder', 'MOCK Try Win', |
| '--build-number', '5000', |
| '--step-name', 'webkit_layout_tests (with patch)', |
| ]], |
| [[ |
| 'python', 'echo', 'optimize-baselines', |
| '--suffixes', 'txt', |
| 'one/flaky-fail.html', |
| ]] |
| ]) |
| |
| def test_trigger_try_jobs(self): |
| # The trigger_try_jobs method just uses git cl to trigger jobs for |
| # the given builders. |
| self.command.trigger_try_jobs(['MOCK Try Linux', 'MOCK Try Win']) |
| self.assertEqual( |
| self.command.git_cl.calls, |
| [['git', 'cl', 'try', '-B', 'luci.chromium.try', |
| '-b', 'MOCK Try Linux', '-b', 'MOCK Try Win']]) |
| self.assertLog([ |
| 'INFO: Triggering try jobs:\n', |
| 'INFO: MOCK Try Linux\n', |
| 'INFO: MOCK Try Win\n', |
| 'INFO: Once all pending try jobs have finished, please re-run\n' |
| 'blink_tool.py rebaseline-cl to fetch new baselines.\n', |
| ]) |
| |
| def test_execute_missing_results_with_no_fill_missing_prompts(self): |
| self.tool.buildbot.set_results(Build('MOCK Try Win', 5000), None) |
| exit_code = self.command.execute(self.command_options(), [], self.tool) |
| self.assertEqual(exit_code, 1) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: Failed to fetch results for "MOCK Try Win".\n', |
| ('INFO: Results URL: https://test-results.appspot.com/data/layout_results/' |
| 'MOCK_Try_Win/5000/webkit_layout_tests%20%28with%20patch%29/layout-test-results/results.html\n'), |
| 'INFO: There are some builders with no results:\n', |
| 'INFO: MOCK Try Win\n', |
| 'INFO: Would you like to continue?\n', |
| 'INFO: Aborting.\n', |
| ]) |
| |
| def test_execute_missing_results_with_fill_missing_continues(self): |
| self.tool.buildbot.set_results(Build('MOCK Try Win', 5000), None) |
| exit_code = self.command.execute( |
| self.command_options(fill_missing=True), |
| ['one/flaky-fail.html'], self.tool) |
| self.assertEqual(exit_code, 0) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: Failed to fetch results for "MOCK Try Win".\n', |
| ('INFO: Results URL: https://test-results.appspot.com/data/layout_results/' |
| 'MOCK_Try_Win/5000/webkit_layout_tests%20%28with%20patch%29/layout-test-results/results.html\n'), |
| 'INFO: There are some builders with no results:\n', |
| 'INFO: MOCK Try Win\n', |
| 'INFO: For one/flaky-fail.html:\n', |
| 'INFO: Using "MOCK Try Linux" build 6000 for test-win-win7.\n', |
| 'INFO: Rebaselining one/flaky-fail.html\n' |
| ]) |
| |
| def test_fill_in_missing_results(self): |
| test_baseline_set = TestBaselineSet(self.tool) |
| test_baseline_set.add('one/flaky-fail.html', Build('MOCK Try Linux', 100)) |
| test_baseline_set.add('one/flaky-fail.html', Build('MOCK Try Win', 200)) |
| self.command.fill_in_missing_results(test_baseline_set) |
| self.assertEqual( |
| test_baseline_set.build_port_pairs('one/flaky-fail.html'), |
| [ |
| (Build('MOCK Try Linux', 100), 'test-linux-trusty'), |
| (Build('MOCK Try Win', 200), 'test-win-win7'), |
| (Build('MOCK Try Linux', 100), 'test-mac-mac10.11'), |
| ]) |
| self.assertLog([ |
| 'INFO: For one/flaky-fail.html:\n', |
| 'INFO: Using "MOCK Try Linux" build 100 for test-mac-mac10.11.\n', |
| ]) |
| |
| def test_fill_in_missing_results_prefers_build_with_same_os_type(self): |
| self.tool.builders = BuilderList({ |
| 'MOCK Foo12': { |
| 'port_name': 'foo-foo12', |
| 'specifiers': ['Foo12', 'Release'], |
| 'is_try_builder': True, |
| }, |
| 'MOCK Foo45': { |
| 'port_name': 'foo-foo45', |
| 'specifiers': ['Foo45', 'Release'], |
| 'is_try_builder': True, |
| }, |
| 'MOCK Bar3': { |
| 'port_name': 'bar-bar3', |
| 'specifiers': ['Bar3', 'Release'], |
| 'is_try_builder': True, |
| }, |
| 'MOCK Bar4': { |
| 'port_name': 'bar-bar4', |
| 'specifiers': ['Bar4', 'Release'], |
| 'is_try_builder': True, |
| }, |
| }) |
| test_baseline_set = TestBaselineSet(self.tool) |
| test_baseline_set.add('one/flaky-fail.html', Build('MOCK Foo12', 100)) |
| test_baseline_set.add('one/flaky-fail.html', Build('MOCK Bar4', 200)) |
| self.command.fill_in_missing_results(test_baseline_set) |
| self.assertEqual( |
| test_baseline_set.build_port_pairs('one/flaky-fail.html'), |
| [ |
| (Build('MOCK Foo12', 100), 'foo-foo12'), |
| (Build('MOCK Bar4', 200), 'bar-bar4'), |
| (Build('MOCK Foo12', 100), 'foo-foo45'), |
| (Build('MOCK Bar4', 200), 'bar-bar3'), |
| ]) |
| self.assertLog([ |
| 'INFO: For one/flaky-fail.html:\n', |
| 'INFO: Using "MOCK Foo12" build 100 for foo-foo45.\n', |
| 'INFO: Using "MOCK Bar4" build 200 for bar-bar3.\n', |
| ]) |
| |
| def test_explicit_builder_list(self): |
| builders = ['MOCK Try Linux', 'MOCK Try Mac'] |
| options = self.command_options(builders=builders) |
| exit_code = self.command.execute(options, [], self.tool) |
| self.assertLog([ |
| 'INFO: Finished try jobs found for all try bots.\n', |
| 'INFO: Rebaselining one/flaky-fail.html\n', |
| 'INFO: Rebaselining one/missing.html\n', |
| 'INFO: Rebaselining one/slow-fail.html\n', |
| 'INFO: Rebaselining one/text-fail.html\n', |
| 'INFO: Rebaselining two/image-fail.html\n', |
| ]) |
| self.assertEqual(exit_code, 0) |
| self.assertEqual(self.command.selected_try_bots, frozenset(builders)) |