diff options
Diffstat (limited to 'crosperf/results_report_unittest.py')
-rwxr-xr-x | crosperf/results_report_unittest.py | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/crosperf/results_report_unittest.py b/crosperf/results_report_unittest.py new file mode 100755 index 00000000..ed5c74fa --- /dev/null +++ b/crosperf/results_report_unittest.py @@ -0,0 +1,415 @@ +#!/usr/bin/env python2 +# +# Copyright 2016 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. + +"""Unittest for the results reporter.""" + +from __future__ import division +from __future__ import print_function + +from StringIO import StringIO + +import collections +import mock +import os +import test_flag +import unittest + +from benchmark_run import MockBenchmarkRun +from cros_utils import logger +from experiment_factory import ExperimentFactory +from experiment_file import ExperimentFile +from machine_manager import MockCrosMachine +from machine_manager import MockMachineManager +from results_cache import MockResult +from results_report import BenchmarkResults +from results_report import HTMLResultsReport +from results_report import JSONResultsReport +from results_report import ParseChromeosImage +from results_report import ParseStandardPerfReport +from results_report import TextResultsReport + + +class FreeFunctionsTest(unittest.TestCase): + """Tests for any free functions in results_report.""" + + def testParseChromeosImage(self): + # N.B. the cases with blank versions aren't explicitly supported by + # ParseChromeosImage. I'm not sure if they need to be supported, but the + # goal of this was to capture existing functionality as much as possible. + base_case = '/my/chroot/src/build/images/x86-generic/R01-1.0.date-time' \ + '/chromiumos_test_image.bin' + self.assertEqual(ParseChromeosImage(base_case), ('R01-1.0', base_case)) + + dir_base_case = os.path.dirname(base_case) + self.assertEqual(ParseChromeosImage(dir_base_case), ('', dir_base_case)) + + buildbot_case = '/my/chroot/chroot/tmp/buildbot-build/R02-1.0.date-time' \ + '/chromiumos_test_image.bin' + buildbot_img = buildbot_case.split('/chroot/tmp')[1] + + self.assertEqual(ParseChromeosImage(buildbot_case), + ('R02-1.0', buildbot_img)) + self.assertEqual(ParseChromeosImage(os.path.dirname(buildbot_case)), + ('', os.path.dirname(buildbot_img))) + + # Ensure we don't act completely insanely given a few mildly insane paths. + fun_case = '/chromiumos_test_image.bin' + self.assertEqual(ParseChromeosImage(fun_case), ('', fun_case)) + + fun_case2 = 'chromiumos_test_image.bin' + self.assertEqual(ParseChromeosImage(fun_case2), ('', fun_case2)) + + +# There are many ways for this to be done better, but the linter complains +# about all of them (that I can think of, at least). +_fake_path_number = [0] +def FakePath(ext): + """Makes a unique path that shouldn't exist on the host system. + + Each call returns a different path, so if said path finds its way into an + error message, it may be easier to track it to its source. + """ + _fake_path_number[0] += 1 + prefix = '/tmp/should/not/exist/%d/' % (_fake_path_number[0], ) + return os.path.join(prefix, ext) + + +def MakeMockExperiment(compiler='gcc'): + """Mocks an experiment using the given compiler.""" + mock_experiment_file = StringIO(""" + board: x86-alex + remote: 127.0.0.1 + perf_args: record -a -e cycles + benchmark: PageCycler { + iterations: 3 + } + + image1 { + chromeos_image: %s + } + + image2 { + remote: 127.0.0.2 + chromeos_image: %s + } + """ % (FakePath('cros_image1.bin'), FakePath('cros_image2.bin'))) + efile = ExperimentFile(mock_experiment_file) + experiment = ExperimentFactory().GetExperiment(efile, + FakePath('working_directory'), + FakePath('log_dir')) + for label in experiment.labels: + label.compiler = compiler + return experiment + + +def _InjectSuccesses(experiment, how_many, keyvals, for_benchmark=0, + label=None): + """Injects successful experiment runs (for each label) into the experiment.""" + # Defensive copy of keyvals, so if it's modified, we'll know. + keyvals = dict(keyvals) + num_configs = len(experiment.benchmarks) * len(experiment.labels) + num_runs = len(experiment.benchmark_runs) // num_configs + + # TODO(gbiv): Centralize the mocking of these, maybe? (It's also done in + # benchmark_run_unittest) + bench = experiment.benchmarks[for_benchmark] + cache_conditions = [] + log_level = 'average' + share_cache = '' + locks_dir = '' + log = logger.GetLogger() + machine_manager = MockMachineManager(FakePath('chromeos_root'), 0, + log_level, locks_dir) + machine_manager.AddMachine('testing_machine') + machine = next(m for m in machine_manager.GetMachines() + if m.name == 'testing_machine') + for label in experiment.labels: + def MakeSuccessfulRun(n): + run = MockBenchmarkRun('mock_success%d' % (n, ), bench, label, + 1 + n + num_runs, cache_conditions, + machine_manager, log, log_level, share_cache) + mock_result = MockResult(log, label, log_level, machine) + mock_result.keyvals = keyvals + run.result = mock_result + return run + + experiment.benchmark_runs.extend(MakeSuccessfulRun(n) + for n in xrange(how_many)) + return experiment + + +class TextResultsReportTest(unittest.TestCase): + """Tests that the output of a text report contains the things we pass in. + + At the moment, this doesn't care deeply about the format in which said + things are displayed. It just cares that they're present. + """ + + def _checkReport(self, email): + num_success = 2 + success_keyvals = {'retval': 0, 'machine': 'some bot', 'a_float': 3.96} + experiment = _InjectSuccesses(MakeMockExperiment(), num_success, + success_keyvals) + text_report = TextResultsReport.FromExperiment(experiment, email=email) \ + .GetReport() + self.assertIn(str(success_keyvals['a_float']), text_report) + self.assertIn(success_keyvals['machine'], text_report) + self.assertIn(MockCrosMachine.CPUINFO_STRING, text_report) + return text_report + + + def testOutput(self): + email_report = self._checkReport(email=True) + text_report = self._checkReport(email=False) + + # Ensure that the reports somehow different. Otherwise, having the + # distinction is useless. + self.assertNotEqual(email_report, text_report) + + +class HTMLResultsReportTest(unittest.TestCase): + """Tests that the output of a HTML report contains the things we pass in. + + At the moment, this doesn't care deeply about the format in which said + things are displayed. It just cares that they're present. + """ + + _TestOutput = collections.namedtuple('TestOutput', ['summary_table', + 'perf_html', + 'chart_js', + 'charts', + 'full_table', + 'experiment_file']) + + @staticmethod + def _GetTestOutput(perf_table, chart_js, summary_table, print_table, + chart_divs, full_table, experiment_file): + # N.B. Currently we don't check chart_js; it's just passed through because + # cros lint complains otherwise. + summary_table = print_table(summary_table, 'HTML') + perf_html = print_table(perf_table, 'HTML') + full_table = print_table(full_table, 'HTML') + return HTMLResultsReportTest._TestOutput(summary_table=summary_table, + perf_html=perf_html, + chart_js=chart_js, + charts=chart_divs, + full_table=full_table, + experiment_file=experiment_file) + + def _GetOutput(self, experiment=None, benchmark_results=None): + with mock.patch('results_report_templates.GenerateHTMLPage') as standin: + if experiment is not None: + HTMLResultsReport.FromExperiment(experiment).GetReport() + else: + HTMLResultsReport(benchmark_results).GetReport() + mod_mock = standin + self.assertEquals(mod_mock.call_count, 1) + # call_args[0] is positional args, call_args[1] is kwargs. + self.assertEquals(mod_mock.call_args[0], tuple()) + fmt_args = mod_mock.call_args[1] + return self._GetTestOutput(**fmt_args) + + def testNoSuccessOutput(self): + output = self._GetOutput(MakeMockExperiment()) + self.assertIn('no result', output.summary_table) + self.assertIn('no result', output.full_table) + self.assertEqual(output.charts, '') + self.assertNotEqual(output.experiment_file, '') + + def testSuccessfulOutput(self): + num_success = 2 + success_keyvals = {'retval': 0, 'a_float': 3.96} + output = self._GetOutput(_InjectSuccesses(MakeMockExperiment(), num_success, + success_keyvals)) + + self.assertNotIn('no result', output.summary_table) + #self.assertIn(success_keyvals['machine'], output.summary_table) + self.assertIn('a_float', output.summary_table) + self.assertIn(str(success_keyvals['a_float']), output.summary_table) + self.assertIn('a_float', output.full_table) + # The _ in a_float is filtered out when we're generating HTML. + self.assertIn('afloat', output.charts) + # And make sure we have our experiment file... + self.assertNotEqual(output.experiment_file, '') + + def testBenchmarkResultFailure(self): + labels = ['label1'] + benchmark_names_and_iterations = [('bench1', 1)] + benchmark_keyvals = {'bench1': [[]]} + results = BenchmarkResults(labels, benchmark_names_and_iterations, + benchmark_keyvals) + output = self._GetOutput(benchmark_results=results) + self.assertIn('no result', output.summary_table) + self.assertEqual(output.charts, '') + self.assertEqual(output.experiment_file, '') + + def testBenchmarkResultSuccess(self): + labels = ['label1'] + benchmark_names_and_iterations = [('bench1', 1)] + benchmark_keyvals = {'bench1': [[{'retval': 1, 'foo': 2.0}]]} + results = BenchmarkResults(labels, benchmark_names_and_iterations, + benchmark_keyvals) + output = self._GetOutput(benchmark_results=results) + self.assertNotIn('no result', output.summary_table) + self.assertIn('bench1', output.summary_table) + self.assertIn('bench1', output.full_table) + self.assertNotEqual(output.charts, '') + self.assertEqual(output.experiment_file, '') + + +class JSONResultsReportTest(unittest.TestCase): + """Tests JSONResultsReport.""" + + REQUIRED_REPORT_KEYS = ('date', 'time', 'label', 'test_name', 'pass') + EXPERIMENT_REPORT_KEYS = ('board', 'chromeos_image', 'chromeos_version', + 'chrome_version', 'compiler') + + @staticmethod + def _GetRequiredKeys(is_experiment): + required_keys = JSONResultsReportTest.REQUIRED_REPORT_KEYS + if is_experiment: + required_keys += JSONResultsReportTest.EXPERIMENT_REPORT_KEYS + return required_keys + + def _CheckRequiredKeys(self, test_output, is_experiment): + required_keys = self._GetRequiredKeys(is_experiment) + for output in test_output: + for key in required_keys: + self.assertIn(key, output) + + def testAllFailedJSONReportOutput(self): + experiment = MakeMockExperiment() + results = JSONResultsReport.FromExperiment(experiment).GetReportObject() + self._CheckRequiredKeys(results, is_experiment=True) + # Nothing succeeded; we don't send anything more than what's required. + required_keys = self._GetRequiredKeys(is_experiment=True) + for result in results: + self.assertItemsEqual(result.iterkeys(), required_keys) + + def testJSONReportOutputWithSuccesses(self): + success_keyvals = { + 'retval': 0, + 'a_float': '2.3', + 'many_floats': [['1.0', '2.0'], ['3.0']], + 'machine': "i'm a pirate" + } + + # 2 is arbitrary. + num_success = 2 + experiment = _InjectSuccesses(MakeMockExperiment(), num_success, + success_keyvals) + results = JSONResultsReport.FromExperiment(experiment).GetReportObject() + self._CheckRequiredKeys(results, is_experiment=True) + + num_passes = num_success * len(experiment.labels) + non_failures = [r for r in results if r['pass']] + self.assertEqual(num_passes, len(non_failures)) + + # TODO(gbiv): ...Is the 3.0 *actually* meant to be dropped? + expected_detailed = {'a_float': 2.3, 'many_floats': [1.0, 2.0]} + for pass_ in non_failures: + self.assertIn('detailed_results', pass_) + self.assertDictEqual(expected_detailed, pass_['detailed_results']) + self.assertIn('machine', pass_) + self.assertEqual(success_keyvals['machine'], pass_['machine']) + + def testFailedJSONReportOutputWithoutExperiment(self): + labels = ['label1'] + benchmark_names_and_iterations = [('bench1', 1), ('bench2', 2), + ('bench3', 1), ('bench4', 0)] + benchmark_keyvals = { + 'bench1': [[{'retval': 1, 'foo': 2.0}]], + 'bench2': [[{'retval': 1, 'foo': 4.0}, {'retval': -1, 'bar': 999}]], + # lack of retval is considered a failure. + 'bench3': [[{}]], + 'bench4': [[]] + } + bench_results = BenchmarkResults(labels, benchmark_names_and_iterations, + benchmark_keyvals) + results = JSONResultsReport(bench_results).GetReportObject() + self._CheckRequiredKeys(results, is_experiment=False) + self.assertFalse(any(r['pass'] for r in results)) + + def testJSONGetReportObeysJSONSettings(self): + labels = ['label1'] + benchmark_names_and_iterations = [('bench1', 1)] + # These can be anything, really. So long as they're distinctive. + separators = (',\t\n\t', ':\t\n\t') + benchmark_keyvals = {'bench1': [[{'retval': 0, 'foo': 2.0}]]} + bench_results = BenchmarkResults(labels, benchmark_names_and_iterations, + benchmark_keyvals) + reporter = JSONResultsReport(bench_results, + json_args={'separators': separators}) + result_str = reporter.GetReport() + self.assertIn(separators[0], result_str) + self.assertIn(separators[1], result_str) + + def testSuccessfulJSONReportOutputWithoutExperiment(self): + labels = ['label1'] + benchmark_names_and_iterations = [('bench1', 1), ('bench2', 2)] + benchmark_keyvals = { + 'bench1': [[{'retval': 0, 'foo': 2.0}]], + 'bench2': [[{'retval': 0, 'foo': 4.0}, {'retval': 0, 'bar': 999}]] + } + bench_results = BenchmarkResults(labels, benchmark_names_and_iterations, + benchmark_keyvals) + results = JSONResultsReport(bench_results).GetReportObject() + self._CheckRequiredKeys(results, is_experiment=False) + self.assertTrue(all(r['pass'] for r in results)) + # Enforce that the results have *some* deterministic order. + keyfn = lambda r: (r['test_name'], r['detailed_results'].get('foo', 5.0)) + sorted_results = sorted(results, key=keyfn) + detailed_results = [r['detailed_results'] for r in sorted_results] + bench1, bench2_foo, bench2_bar = detailed_results + self.assertEqual(bench1['foo'], 2.0) + self.assertEqual(bench2_foo['foo'], 4.0) + self.assertEqual(bench2_bar['bar'], 999) + self.assertNotIn('bar', bench1) + self.assertNotIn('bar', bench2_foo) + self.assertNotIn('foo', bench2_bar) + + +class PerfReportParserTest(unittest.TestCase): + """Tests for the perf report parser in results_report.""" + @staticmethod + def _ReadRealPerfReport(): + my_dir = os.path.dirname(os.path.realpath(__file__)) + with open(os.path.join(my_dir, 'perf_files/perf.data.report.0')) as f: + return f.read() + + def testParserParsesRealWorldPerfReport(self): + report = ParseStandardPerfReport(self._ReadRealPerfReport()) + self.assertItemsEqual(['cycles', 'instructions'], report.keys()) + + # Arbitrarily selected known percentages from the perf report. + known_cycles_percentages = { + '0xffffffffa4a1f1c9': 0.66, + '0x0000115bb7ba9b54': 0.47, + '0x0000000000082e08': 0.00, + '0xffffffffa4a13e63': 0.00, + } + report_cycles = report['cycles'] + self.assertEqual(len(report_cycles), 214) + for k, v in known_cycles_percentages.iteritems(): + self.assertIn(k, report_cycles) + self.assertEqual(v, report_cycles[k]) + + known_instrunctions_percentages = { + '0x0000115bb6c35d7a': 1.65, + '0x0000115bb7ba9b54': 0.67, + '0x0000000000024f56': 0.00, + '0xffffffffa4a0ee03': 0.00, + } + report_instructions = report['instructions'] + self.assertEqual(len(report_instructions), 492) + for k, v in known_instrunctions_percentages.iteritems(): + self.assertIn(k, report_instructions) + self.assertEqual(v, report_instructions[k]) + + +if __name__ == '__main__': + test_flag.SetTestMode(True) + unittest.main() |