Index: tracing/tracing/metrics/compare_samples_unittest.py |
diff --git a/tracing/tracing/metrics/compare_samples_unittest.py b/tracing/tracing/metrics/compare_samples_unittest.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..39caa202b7197e3d03d7263b47d0bd5dd074370c |
--- /dev/null |
+++ b/tracing/tracing/metrics/compare_samples_unittest.py |
@@ -0,0 +1,222 @@ |
+# 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 math |
+import os |
+import random |
+import tempfile |
+import unittest |
+ |
+from tracing.metrics import compare_samples |
+ |
+ |
+REJECT_THE_NULL = 'REJECT_THE_NULL' |
+FAIL_TO_REJECT_THE_NULL = 'FAIL_TO_REJECT_THE_NULL' |
+NEED_MORE_DATA = 'NEED_MORE_DATA' |
+ |
+ |
+def Mean(l): |
+ if len(l): |
+ return float(sum(l))/len(l) |
+ return 0 |
+ |
+ |
+class CompareSamplesUnittest(unittest.TestCase): |
+ def setUp(self): |
+ self._tempfiles = [] |
+ self._tempdir = tempfile.mkdtemp() |
+ |
+ def tearDown(self): |
+ for tf in self._tempfiles: |
+ try: |
+ os.remove(tf) |
+ except OSError: |
+ pass |
+ try: |
+ os.rmdir(self._tempdir) |
+ except OSError: |
+ pass |
+ |
+ def NewJsonTempfile(self, jsonable_contents): |
+ _, new_json_file = tempfile.mkstemp( |
+ suffix='.json', |
+ dir=self._tempdir, |
+ text=True) |
+ self._tempfiles.append(new_json_file) |
+ with open(new_json_file, 'w') as f: |
+ json.dump(jsonable_contents, f) |
+ return new_json_file |
+ |
+ def MakeMultipleChartJSONHistograms(self, metric, seed, mu, sigma, n, m): |
+ result = [] |
+ random.seed(seed) |
+ for _ in range(m): |
+ result.append(self.MakeChartJSONHistogram(metric, mu, sigma, n)) |
+ return result |
+ |
+ def MakeChartJSONHistogram(self, metric, mu, sigma, n): |
+ """Creates a histogram for a normally distributed pseudo-random sample. |
+ |
+ This function creates a deterministic pseudo-random sample and stores it in |
+ chartjson histogram format to facilitate the testing of the sample |
+ comparison logic. |
+ |
+ For simplicity we use sqrt(n) buckets with equal widths. |
+ |
+ Args: |
+ metric (str pair): name of chart, name of the trace. |
+ seed (hashable obj): to make the sequences deterministic we seed the RNG. |
+ mu (float): desired mean for the sample |
+ sigma (float): desired standard deviation for the sample |
+ n (int): number of values to generate. |
+ """ |
+ chart_name, trace_name = metric |
+ values = [random.gauss(mu, sigma) for _ in range(n)] |
+ bucket_count = int(math.ceil(math.sqrt(len(values)))) |
+ width = (max(values) - min(values))/(bucket_count - 1) |
+ prev_bucket = min(values) |
+ buckets = [] |
+ for _ in range(bucket_count): |
+ buckets.append({'low': prev_bucket, |
+ 'high': prev_bucket + width, |
+ 'count': 0}) |
+ prev_bucket += width |
+ for value in values: |
+ for bucket in buckets: |
+ if value >= bucket['low'] and value < bucket['high']: |
+ bucket['count'] += 1 |
+ break |
+ charts = { |
+ 'charts': { |
+ chart_name: { |
+ trace_name: { |
+ 'type': 'histogram', |
+ 'buckets': buckets |
+ } |
+ } |
+ } |
+ } |
+ return self.NewJsonTempfile(charts) |
+ |
+ def MakeChart(self, metric, seed, mu, sigma, n): |
+ """Creates a normally distributed pseudo-random sample. (continuous). |
+ |
+ This function creates a deterministic pseudo-random sample and stores it in |
+ chartjson format to facilitate the testing of the sample comparison logic. |
+ |
+ Args: |
+ metric (str pair): name of chart, name of the trace. |
+ seed (hashable obj): to make the sequences deterministic we seed the RNG. |
+ mu (float): desired mean for the sample |
+ sigma (float): desired standard deviation for the sample |
+ n (int): number of values to generate. |
+ """ |
+ chart_name, trace_name = metric |
+ random.seed(seed) |
+ values = [random.gauss(mu, sigma) for _ in range(n)] |
+ charts = { |
+ 'charts': { |
+ chart_name: { |
+ trace_name: { |
+ 'type': 'list_of_scalar_values', |
+ 'values': values} |
+ } |
+ } |
+ } |
+ return self.NewJsonTempfile(charts) |
+ |
+ def testCompareClearRegression(self): |
+ metric = ('some_chart', 'some_trace') |
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', |
+ mu=10, sigma=1, n=10)]) |
+ higher_values = ','.join([self.MakeChart(metric=metric, seed='higher', |
+ mu=20, sigma=2, n=10)]) |
+ result = json.loads(compare_samples.CompareSamples( |
+ lower_values, higher_values, '/'.join(metric))) |
+ self.assertEqual(result['result'], REJECT_THE_NULL) |
+ |
+ def testCompareUnlikelyRegressionWithMultipleRuns(self): |
+ metric = ('some_chart', 'some_trace') |
+ lower_values = ','.join([ |
+ self.MakeChart( |
+ metric=metric, seed='lower%d' % i, mu=10, sigma=1, n=5) |
+ for i in range(4)]) |
+ higher_values = ','.join([ |
+ self.MakeChart( |
+ metric=metric, seed='higher%d' % i, mu=10.01, sigma=0.95, n=5) |
+ for i in range(4)]) |
+ result = json.loads(compare_samples.CompareSamples( |
+ lower_values, higher_values, '/'.join(metric))) |
+ self.assertEqual(result['result'], FAIL_TO_REJECT_THE_NULL) |
+ |
+ def testCompareInsufficientData(self): |
+ metric = ('some_chart', 'some_trace') |
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', |
+ mu=10, sigma=1, n=5)]) |
+ higher_values = ','.join([self.MakeChart(metric=metric, seed='higher', |
+ mu=10.40, sigma=0.95, n=5)]) |
+ result = json.loads(compare_samples.CompareSamples( |
+ lower_values, higher_values, '/'.join(metric))) |
+ self.assertEqual(result['result'], NEED_MORE_DATA) |
+ |
+ def testCompareMissingFile(self): |
+ metric = ('some_chart', 'some_trace') |
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', |
+ mu=10, sigma=1, n=5)]) |
+ higher_values = '/path/does/not/exist.json' |
+ with self.assertRaises(RuntimeError): |
+ compare_samples.CompareSamples( |
+ lower_values, higher_values, '/'.join(metric)) |
+ |
+ def testCompareMissingMetric(self): |
+ metric = ('some_chart', 'some_trace') |
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', |
+ mu=10, sigma=1, n=5)]) |
+ higher_values = ','.join([self.MakeChart(metric=metric, seed='higher', |
+ mu=20, sigma=2, n=5)]) |
+ metric = ('some_chart', 'missing_trace') |
+ with self.assertRaises(RuntimeError): |
+ compare_samples.CompareSamples( |
+ lower_values, higher_values, '/'.join(metric)) |
+ |
+ def testCompareBadChart(self): |
+ metric = ('some_chart', 'some_trace') |
+ lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', |
+ mu=10, sigma=1, n=5)]) |
+ higher_values = self.NewJsonTempfile(['obviously', 'not', 'a', 'chart]']) |
+ with self.assertRaises(RuntimeError): |
+ compare_samples.CompareSamples( |
+ lower_values, higher_values, '/'.join(metric)) |
+ |
+ def testCompareValuesets(self): |
+ vs = os.path.join(os.path.dirname(__file__), 'test-valueset.json') |
+ result = compare_samples.CompareSamples( |
+ vs, vs, 'timeToFirstContentfulPaint/pcv1-cold/' |
+ 'http___www.rambler.ru_', data_format='valueset') |
+ result = json.loads(result) |
+ self.assertEqual(result['result'], NEED_MORE_DATA) |
+ self.assertAlmostEqual(Mean(result['sample_a']), 75.3177999958396) |
+ self.assertAlmostEqual(Mean(result['sample_b']), 75.3177999958396) |
+ |
+ def testCompareBuildbotOutput(self): |
+ bb = os.path.join(os.path.dirname(__file__), 'test-buildbot.txt') |
+ result = compare_samples.CompareSamples( |
+ bb, bb, 'DrawCallPerf_gl/score', |
+ data_format='buildbot') |
+ result = json.loads(result) |
+ self.assertEqual(result['result'], NEED_MORE_DATA) |
+ self.assertEqual(Mean(result['sample_a']), 4123) |
+ self.assertEqual(Mean(result['sample_b']), 4123) |
+ |
+ def testCompareChartJsonHistogram(self): |
+ metric = ('some_chart', 'some_trace') |
+ lower_values = ','.join(self.MakeMultipleChartJSONHistograms( |
+ metric=metric, seed='lower', mu=10, sigma=1, n=100, m=10)) |
+ higher_values = ','.join(self.MakeMultipleChartJSONHistograms( |
+ metric=metric, seed='higher', mu=20, sigma=2, n=100, m=10)) |
+ result = json.loads(compare_samples.CompareSamples( |
+ lower_values, higher_values, '/'.join(metric))) |
+ self.assertEqual(result['result'], REJECT_THE_NULL) |
+ |