# Copyright 2014 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 os import re import shutil import sys import urlparse import unittest SRC = os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir) sys.path.append(os.path.join(SRC, 'third_party', 'pymock')) import bisect_perf_regression import bisect_results import bisect_state import bisect_utils import fetch_build import mock import source_control # Regression confidence: 0% CLEAR_NON_REGRESSION = [ # Mean: 30.223 Std. Dev.: 11.383 [[16.886], [16.909], [16.99], [17.723], [17.952], [18.118], [19.028], [19.552], [21.954], [38.573], [38.839], [38.965], [40.007], [40.572], [41.491], [42.002], [42.33], [43.109], [43.238]], # Mean: 34.76 Std. Dev.: 11.516 [[16.426], [17.347], [20.593], [21.177], [22.791], [27.843], [28.383], [28.46], [29.143], [40.058], [40.303], [40.558], [41.918], [42.44], [45.223], [46.494], [50.002], [50.625], [50.839]] ] # Regression confidence: ~ 90% ALMOST_REGRESSION = [ # Mean: 30.042 Std. Dev.: 2.002 [[26.146], [28.04], [28.053], [28.074], [28.168], [28.209], [28.471], [28.652], [28.664], [30.862], [30.973], [31.002], [31.897], [31.929], [31.99], [32.214], [32.323], [32.452], [32.696]], # Mean: 33.008 Std. Dev.: 4.265 [[34.963], [30.741], [39.677], [39.512], [34.314], [31.39], [34.361], [25.2], [30.489], [29.434]] ] # Regression confidence: ~ 98% BARELY_REGRESSION = [ # Mean: 28.828 Std. Dev.: 1.993 [[26.96], [27.605], [27.768], [27.829], [28.006], [28.206], [28.393], [28.911], [28.933], [30.38], [30.462], [30.808], [31.74], [31.805], [31.899], [32.077], [32.454], [32.597], [33.155]], # Mean: 31.156 Std. Dev.: 1.980 [[28.729], [29.112], [29.258], [29.454], [29.789], [30.036], [30.098], [30.174], [30.534], [32.285], [32.295], [32.552], [32.572], [32.967], [33.165], [33.403], [33.588], [33.744], [34.147], [35.84]] ] # Regression confidence: 99.5% CLEAR_REGRESSION = [ # Mean: 30.254 Std. Dev.: 2.987 [[26.494], [26.621], [26.701], [26.997], [26.997], [27.05], [27.37], [27.488], [27.556], [31.846], [32.192], [32.21], [32.586], [32.596], [32.618], [32.95], [32.979], [33.421], [33.457], [34.97]], # Mean: 33.190 Std. Dev.: 2.972 [[29.547], [29.713], [29.835], [30.132], [30.132], [30.33], [30.406], [30.592], [30.72], [34.486], [35.247], [35.253], [35.335], [35.378], [35.934], [36.233], [36.41], [36.947], [37.982]] ] # Regression confidence > 95%, taken from: crbug.com/434318 # Specifically from Builder android_nexus10_perf_bisect Build #1198 MULTIPLE_VALUES = [ [ [18.916, 22.371, 8.527, 5.877, 5.407, 9.476, 8.100, 5.334, 4.507, 4.842, 8.485, 8.308, 27.490, 4.560, 4.804, 23.068, 17.577, 17.346, 26.738, 60.330, 32.307, 5.468, 27.803, 27.373, 17.823, 5.158, 27.439, 5.236, 11.413], [18.999, 22.642, 8.158, 5.995, 5.495, 9.499, 8.092, 5.324, 4.468, 4.788, 8.248, 7.853, 27.533, 4.410, 4.622, 22.341, 22.313, 17.072, 26.731, 57.513, 33.001, 5.500, 28.297, 27.277, 26.462, 5.009, 27.361, 5.130, 10.955] ], [ [18.238, 22.365, 8.555, 5.939, 5.437, 9.463, 7.047, 5.345, 4.517, 4.796, 8.593, 7.901, 27.499, 4.378, 5.040, 4.904, 4.816, 4.828, 4.853, 57.363, 34.184, 5.482, 28.190, 27.290, 26.694, 5.099, 4.905, 5.290, 4.813], [18.301, 22.522, 8.035, 6.021, 5.565, 9.037, 6.998, 5.321, 4.485, 4.768, 8.397, 7.865, 27.636, 4.640, 5.015, 4.962, 4.933, 4.977, 4.961, 60.648, 34.593, 5.538, 28.454, 27.297, 26.490, 5.099, 5, 5.247, 4.945], [18.907, 23.368, 8.100, 6.169, 5.621, 9.971, 8.161, 5.331, 4.513, 4.837, 8.255, 7.852, 26.209, 4.388, 5.045, 5.029, 5.032, 4.946, 4.973, 60.334, 33.377, 5.499, 28.275, 27.550, 26.103, 5.108, 4.951, 5.285, 4.910], [18.715, 23.748, 8.128, 6.148, 5.691, 9.361, 8.106, 5.334, 4.528, 4.965, 8.261, 7.851, 27.282, 4.391, 4.949, 4.981, 4.964, 4.935, 4.933, 60.231, 33.361, 5.489, 28.106, 27.457, 26.648, 5.108, 4.963, 5.272, 4.954] ] ] # Default options for the dry run DEFAULT_OPTIONS = { 'debug_ignore_build': True, 'debug_ignore_sync': True, 'debug_ignore_perf_test': True, 'debug_ignore_regression_confidence': True, 'command': 'fake_command', 'metric': 'fake/metric', 'good_revision': 280000, 'bad_revision': 280005, } # This global is a placeholder for a generator to be defined by the test cases # that use _MockRunTests. _MockResultsGenerator = (x for x in []) def _MakeMockRunTests(bisect_mode_is_return_code=False): def _MockRunTests(*args, **kwargs): # pylint: disable=unused-argument return _FakeTestResult( _MockResultsGenerator.next(), bisect_mode_is_return_code) return _MockRunTests def _FakeTestResult(values, bisect_mode_is_return_code): mean = 0.0 if bisect_mode_is_return_code: mean = 0 if (all(v == 0 for v in values)) else 1 result_dict = {'mean': mean, 'std_err': 0.0, 'std_dev': 0.0, 'values': values} success_code = 0 return (result_dict, success_code) def _SampleBisecResult(opts): revisions = [ 'ae7ef14ba2d9b5ef0d2c1c092ec98a417e44740d' 'ab55ead638496b061c9de61685b982f7cea38ca7', '89aa0c99e4b977b9a4f992ac14da0d6624f7316e'] state = bisect_state.BisectState(depot='chromium', revisions=revisions) depot_registry = bisect_perf_regression.DepotDirectoryRegistry('/mock/src') results = bisect_results.BisectResults( bisect_state=state, depot_registry=depot_registry, opts=opts, runtime_warnings=[]) results.confidence = 99.9 results.culprit_revisions = [( 'ab55ead638496b061c9de61685b982f7cea38ca7', { 'date': 'Thu, 26 Jun 2014 14:29:49 +0000', 'body': 'Fix', 'author': 'author@chromium.org', 'subject': 'Fix', 'email': 'author@chromium.org', }, 'chromium')] return results def _GetMockCallArg(function_mock, call_index): """Gets the list of called arguments for call at |call_index|. Args: function_mock: A Mock object. call_index: The index at which the mocked function was called. Returns: The called argument list. """ call_args_list = function_mock.call_args_list if not call_args_list or len(call_args_list) <= call_index: return None args, _ = call_args_list[call_index] return args def _GetBisectPerformanceMetricsInstance(options_dict): """Returns an instance of the BisectPerformanceMetrics class.""" opts = bisect_perf_regression.BisectOptions.FromDict(options_dict) return bisect_perf_regression.BisectPerformanceMetrics(opts, os.getcwd()) def _GetExtendedOptions(improvement_dir, fake_first, ignore_confidence=True, **extra_opts): """Returns the a copy of the default options dict plus some options.""" result = dict(DEFAULT_OPTIONS) result.update({ 'improvement_direction': improvement_dir, 'debug_fake_first_test_mean': fake_first, 'debug_ignore_regression_confidence': ignore_confidence }) result.update(extra_opts) return result def _GenericDryRun(options, print_results=False): """Performs a dry run of the bisector. Args: options: Dictionary containing the options for the bisect instance. print_results: Boolean telling whether to call FormatAndPrintResults. Returns: The results dictionary as returned by the bisect Run method. """ _AbortIfThereAreStagedChanges() # Disable rmtree to avoid deleting local trees. old_rmtree = shutil.rmtree shutil.rmtree = lambda path, on_error: None # git reset HEAD may be run during the dry run, which removes staged changes. try: bisect_instance = _GetBisectPerformanceMetricsInstance(options) results = bisect_instance.Run( bisect_instance.opts.command, bisect_instance.opts.bad_revision, bisect_instance.opts.good_revision, bisect_instance.opts.metric) if print_results: bisect_instance.printer.FormatAndPrintResults(results) return results finally: shutil.rmtree = old_rmtree def _AbortIfThereAreStagedChanges(): """Exits the test prematurely if there are staged changes.""" # The output of "git status --short" will be an empty string if there are # no staged changes in the current branch. Untracked files are ignored # because when running the presubmit on the trybot there are sometimes # untracked changes to the run-perf-test.cfg and bisect.cfg files. status_output = bisect_utils.CheckRunGit( ['status', '--short', '--untracked-files=no']) if status_output: print 'There are un-committed changes in the current branch.' print 'Aborting the tests to avoid destroying local changes. Changes:' print status_output sys.exit(1) class BisectPerfRegressionTest(unittest.TestCase): """Test case for other functions and classes in bisect-perf-regression.py.""" def setUp(self): self.cwd = os.getcwd() os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir))) def tearDown(self): os.chdir(self.cwd) def testBisectOptionsCanPrintHelp(self): """Tests that the argument parser can be made and can print help.""" bisect_options = bisect_perf_regression.BisectOptions() parser = bisect_options._CreateCommandLineParser() parser.format_help() def testParseDEPSStringManually(self): """Tests DEPS parsing.""" deps_file_contents = """ vars = { 'ffmpeg_hash': '@ac4a9f31fe2610bd146857bbd55d7a260003a888', 'webkit_url': 'https://chromium.googlesource.com/chromium/blink.git', 'git_url': 'https://chromium.googlesource.com', 'webkit_rev': '@e01ac0a267d1017288bc67fa3c366b10469d8a24', 'angle_revision': '74697cf2064c0a2c0d7e1b1b28db439286766a05' }""" # Should only expect SVN/git revisions to come through, and URLs should be # filtered out. expected_vars_dict = { 'ffmpeg_hash': '@ac4a9f31fe2610bd146857bbd55d7a260003a888', 'webkit_rev': '@e01ac0a267d1017288bc67fa3c366b10469d8a24', 'angle_revision': '74697cf2064c0a2c0d7e1b1b28db439286766a05' } # Testing private function. # pylint: disable=W0212 vars_dict = bisect_perf_regression._ParseRevisionsFromDEPSFileManually( deps_file_contents) self.assertEqual(vars_dict, expected_vars_dict) def _AssertParseResult(self, expected_values, result_string): """Asserts some values are parsed from a RESULT line.""" results_template = ('RESULT other_chart: other_trace= 123 count\n' 'RESULT my_chart: my_trace= %(value)s\n') results = results_template % {'value': result_string} metric = ['my_chart', 'my_trace'] # Testing private function. # pylint: disable=W0212 values = bisect_perf_regression._TryParseResultValuesFromOutput( metric, results) self.assertEqual(expected_values, values) def testTryParseResultValuesFromOutput_WithSingleValue(self): """Tests result pattern <*>RESULT : = """ self._AssertParseResult([66.88], '66.88 kb') self._AssertParseResult([66.88], '66.88 ') self._AssertParseResult([-66.88], '-66.88 kb') self._AssertParseResult([66], '66 kb') self._AssertParseResult([0.66], '.66 kb') self._AssertParseResult([], '. kb') self._AssertParseResult([], 'aaa kb') def testTryParseResultValuesFromOutput_WithMultiValue(self): """Tests result pattern <*>RESULT : = [,, ..]""" self._AssertParseResult([66.88], '[66.88] kb') self._AssertParseResult([66.88, 99.44], '[66.88, 99.44]kb') self._AssertParseResult([66.88, 99.44], '[ 66.88, 99.44 ]') self._AssertParseResult([-66.88, 99.44], '[-66.88, 99.44] kb') self._AssertParseResult([-66, 99], '[-66,99] kb') self._AssertParseResult([-66, 99], '[-66,99,] kb') self._AssertParseResult([-66, 0.99], '[-66,.99] kb') self._AssertParseResult([], '[] kb') self._AssertParseResult([], '[-66,abc] kb') def testTryParseResultValuesFromOutputWithMeanStd(self): """Tests result pattern <*>RESULT : = {