• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Tests for afdo_prof_analysis."""
8
9from __future__ import print_function
10
11import random
12import io
13import unittest
14
15from afdo_tools.bisection import afdo_prof_analysis as analysis
16
17
18class AfdoProfAnalysisTest(unittest.TestCase):
19  """Class for testing AFDO Profile Analysis"""
20  bad_items = {'func_a': '1', 'func_b': '3', 'func_c': '5'}
21  good_items = {'func_a': '2', 'func_b': '4', 'func_d': '5'}
22  random.seed(13)  # 13 is an arbitrary choice. just for consistency
23  # add some extra info to make tests more reflective of real scenario
24  for num in range(128):
25    func_name = 'func_extra_%d' % num
26    # 1/3 to both, 1/3 only to good, 1/3 only to bad
27    rand_val = random.randint(1, 101)
28    if rand_val < 67:
29      bad_items[func_name] = 'test_data'
30    if rand_val < 34 or rand_val >= 67:
31      good_items[func_name] = 'test_data'
32
33  analysis.random.seed(5)  # 5 is an arbitrary choice. For consistent testing
34
35  def test_text_to_json(self):
36    test_data = io.StringIO('deflate_slow:87460059:3\n'
37                            ' 3: 24\n'
38                            ' 14: 54767\n'
39                            ' 15: 664 fill_window:22\n'
40                            ' 16: 661\n'
41                            ' 19: 637\n'
42                            ' 41: 36692 longest_match:36863\n'
43                            ' 44: 36692\n'
44                            ' 44.2: 5861\n'
45                            ' 46: 13942\n'
46                            ' 46.1: 14003\n')
47    expected = {
48        'deflate_slow': ':87460059:3\n'
49                        ' 3: 24\n'
50                        ' 14: 54767\n'
51                        ' 15: 664 fill_window:22\n'
52                        ' 16: 661\n'
53                        ' 19: 637\n'
54                        ' 41: 36692 longest_match:36863\n'
55                        ' 44: 36692\n'
56                        ' 44.2: 5861\n'
57                        ' 46: 13942\n'
58                        ' 46.1: 14003\n'
59    }
60    actual = analysis.text_to_json(test_data)
61    self.assertEqual(actual, expected)
62    test_data.close()
63
64  def test_text_to_json_empty_afdo(self):
65    expected = {}
66    actual = analysis.text_to_json('')
67    self.assertEqual(actual, expected)
68
69  def test_json_to_text(self):
70    example_prof = {'func_a': ':1\ndata\n', 'func_b': ':2\nmore data\n'}
71    expected_text = 'func_a:1\ndata\nfunc_b:2\nmore data\n'
72    self.assertEqual(analysis.json_to_text(example_prof), expected_text)
73
74  def test_bisect_profiles(self):
75
76    # mock run of external script with arbitrarily-chosen bad profile vals
77    # save_run specified and unused b/c afdo_prof_analysis.py
78    # will call with argument explicitly specified
79    # pylint: disable=unused-argument
80    class DeciderClass(object):
81      """Class for this tests's decider."""
82
83      def run(self, prof, save_run=False):
84        if '1' in prof['func_a'] or '3' in prof['func_b']:
85          return analysis.StatusEnum.BAD_STATUS
86        return analysis.StatusEnum.GOOD_STATUS
87
88    results = analysis.bisect_profiles_wrapper(DeciderClass(), self.good_items,
89                                               self.bad_items)
90    self.assertEqual(results['individuals'], sorted(['func_a', 'func_b']))
91    self.assertEqual(results['ranges'], [])
92
93  def test_range_search(self):
94
95    # arbitrarily chosen functions whose values in the bad profile constitute
96    # a problematic pair
97    # pylint: disable=unused-argument
98    class DeciderClass(object):
99      """Class for this tests's decider."""
100
101      def run(self, prof, save_run=False):
102        if '1' in prof['func_a'] and '3' in prof['func_b']:
103          return analysis.StatusEnum.BAD_STATUS
104        return analysis.StatusEnum.GOOD_STATUS
105
106    # put the problematic combination in separate halves of the common funcs
107    # so that non-bisecting search is invoked for its actual use case
108    common_funcs = [func for func in self.good_items if func in self.bad_items]
109    common_funcs.remove('func_a')
110    common_funcs.insert(0, 'func_a')
111    common_funcs.remove('func_b')
112    common_funcs.append('func_b')
113
114    problem_range = analysis.range_search(DeciderClass(), self.good_items,
115                                          self.bad_items, common_funcs, 0,
116                                          len(common_funcs))
117
118    self.assertEqual(['func_a', 'func_b'], problem_range)
119
120  def test_check_good_not_bad(self):
121    func_in_good = 'func_c'
122
123    # pylint: disable=unused-argument
124    class DeciderClass(object):
125      """Class for this tests's decider."""
126
127      def run(self, prof, save_run=False):
128        if func_in_good in prof:
129          return analysis.StatusEnum.GOOD_STATUS
130        return analysis.StatusEnum.BAD_STATUS
131
132    self.assertTrue(
133        analysis.check_good_not_bad(DeciderClass(), self.good_items,
134                                    self.bad_items))
135
136  def test_check_bad_not_good(self):
137    func_in_bad = 'func_d'
138
139    # pylint: disable=unused-argument
140    class DeciderClass(object):
141      """Class for this tests's decider."""
142
143      def run(self, prof, save_run=False):
144        if func_in_bad in prof:
145          return analysis.StatusEnum.BAD_STATUS
146        return analysis.StatusEnum.GOOD_STATUS
147
148    self.assertTrue(
149        analysis.check_bad_not_good(DeciderClass(), self.good_items,
150                                    self.bad_items))
151
152
153if __name__ == '__main__':
154  unittest.main()
155