1#!/usr/bin/python3 2 3"""Unit tests for the perf_uploader.py module. 4 5""" 6 7from __future__ import absolute_import 8from __future__ import division 9from __future__ import print_function 10 11import json 12import os 13import tempfile 14import unittest 15 16import common 17from autotest_lib.tko import models as tko_models 18from autotest_lib.tko.perf_upload import perf_uploader 19import six 20 21 22class test_aggregate_iterations(unittest.TestCase): 23 """Tests for the aggregate_iterations function.""" 24 25 _PERF_ITERATION_DATA = { 26 '1': [ 27 { 28 'description': 'metric1', 29 'value': 1, 30 'stddev': 0.0, 31 'units': 'units1', 32 'higher_is_better': True, 33 'graph': None 34 }, 35 { 36 'description': 'metric2', 37 'value': 10, 38 'stddev': 0.0, 39 'units': 'units2', 40 'higher_is_better': True, 41 'graph': 'graph1', 42 }, 43 { 44 'description': 'metric2', 45 'value': 100, 46 'stddev': 1.7, 47 'units': 'units3', 48 'higher_is_better': False, 49 'graph': 'graph2', 50 } 51 ], 52 '2': [ 53 { 54 'description': 'metric1', 55 'value': 2, 56 'stddev': 0.0, 57 'units': 'units1', 58 'higher_is_better': True, 59 'graph': None, 60 }, 61 { 62 'description': 'metric2', 63 'value': 20, 64 'stddev': 0.0, 65 'units': 'units2', 66 'higher_is_better': True, 67 'graph': 'graph1', 68 }, 69 { 70 'description': 'metric2', 71 'value': 200, 72 'stddev': 21.2, 73 'units': 'units3', 74 'higher_is_better': False, 75 'graph': 'graph2', 76 } 77 ], 78 } 79 80 81 def setUp(self): 82 """Sets up for each test case.""" 83 self._perf_values = [] 84 for iter_num, iter_data in six.iteritems(self._PERF_ITERATION_DATA): 85 self._perf_values.append( 86 tko_models.perf_value_iteration(iter_num, iter_data)) 87 88 89 90class test_json_config_file(unittest.TestCase): 91 """Tests for the JSON-formatted presentation config file.""" 92 93 def test_parse_json(self): 94 """Verifies _parse_config_file function.""" 95 perf_uploader._parse_config_file( 96 perf_uploader._PRESENTATION_CONFIG_FILE) 97 98 def test_proper_config(self): 99 """Verifies configs have either autotest_name or autotest_regex.""" 100 json_obj = [] 101 try: 102 with open(perf_uploader._PRESENTATION_CONFIG_FILE, 'r') as fp: 103 json_obj = json.load(fp) 104 except: 105 self.fail('Presentation config file could not be parsed as JSON.') 106 107 for entry in json_obj: 108 if 'autotest_name' not in entry and 'autotest_regex' not in entry: 109 self.fail('Missing autotest_name or autotest_regex field for ' 110 'test %s.' % entry) 111 112 113 def test_proper_json(self): 114 """Verifies the file can be parsed as proper JSON.""" 115 try: 116 with open(perf_uploader._PRESENTATION_CONFIG_FILE, 'r') as fp: 117 json.load(fp) 118 except: 119 self.fail('Presentation config file could not be parsed as JSON.') 120 121 122 def test_required_main_name(self): 123 """Verifies that main name must be specified.""" 124 json_obj = [] 125 try: 126 with open(perf_uploader._PRESENTATION_CONFIG_FILE, 'r') as fp: 127 json_obj = json.load(fp) 128 except: 129 self.fail('Presentation config file could not be parsed as JSON.') 130 for entry in json_obj: 131 if not 'main_name' in entry: 132 self.fail('Missing main field for test %s.' % 133 entry['autotest_name']) 134 135class test_get_image_board_name(unittest.TestCase): 136 """Tests for retrieving the image board name.""" 137 def test_normal_platform(self): 138 """Verify image board name is equal to the platform in normal image.""" 139 platform = 'veyron_jerry' 140 image = 'veyron_jerry-release/R78-12428.0.0' 141 self.assertEqual(perf_uploader._get_image_board_name(platform, image), 142 'veyron_jerry') 143 144 def test_empty_platform(self): 145 """Verify image board name is equal to the platform.""" 146 platform = '' 147 image = '-release/R78-12428.0.0' 148 self.assertEqual(perf_uploader._get_image_board_name(platform, image), 149 '') 150 151 def test_specifc_image_suffix_found(self): 152 """Verify image board name is reflecting the running image.""" 153 platform = 'veyron_jerry' 154 image = 'veyron_jerry-kernelnext-release/R78-12419.0.0' 155 self.assertEqual(perf_uploader._get_image_board_name(platform, image), 156 'veyron_jerry-kernelnext') 157 image = 'veyron_jerry-arcnext-release/R78-12419.0.0' 158 self.assertEqual(perf_uploader._get_image_board_name(platform, image), 159 'veyron_jerry-arcnext') 160 image = 'veyron_jerry-arcvm-release/R78-12419.0.0' 161 self.assertEqual(perf_uploader._get_image_board_name(platform, image), 162 'veyron_jerry-arcvm') 163 164 165class test_gather_presentation_info(unittest.TestCase): 166 """Tests for the gather_presentation_info function.""" 167 168 _PRESENT_INFO = { 169 'test_name': { 170 'main_name': 'new_main_name', 171 'dashboard_test_name': 'new_test_name', 172 } 173 } 174 175 _PRESENT_INFO_MISSING_MAIN = { 176 'test_name': { 177 'dashboard_test_name': 'new_test_name', 178 } 179 } 180 181 _PRESENT_INFO_REGEX = { 182 'test_name.*': { 183 'main_name': 'new_main_name', 184 'dashboard_test_name': 'new_test_name', 185 } 186 } 187 188 _PRESENT_INFO_COLLISION = { 189 'test_name.*': { 190 'main_name': 'new_main_name', 191 'dashboard_test_name': 'new_test_name', 192 }, 193 'test_name-test.*': { 194 'main_name': 'new_main_name', 195 'dashboard_test_name': 'new_test_name', 196 }, 197 } 198 199 def test_test_selection_collision(self): 200 """Verifies error when multiple entry refers to the same test.""" 201 try: 202 result = perf_uploader._gather_presentation_info( 203 self._PRESENT_INFO_COLLISION, 'test_name-test-23') 204 self.fail('PerfUploadingError is expected if more than one entry ' 205 'refer to the same test.') 206 except perf_uploader.PerfUploadingError: 207 return 208 209 def test_test_name_regex_specified(self): 210 """Verifies gathers presentation info for regex search correctly""" 211 for test_name in ['test_name.arm.7.1', 'test_name.x86.7.1']: 212 result = perf_uploader._gather_presentation_info( 213 self._PRESENT_INFO, 'test_name_P') 214 self.assertTrue( 215 all([key in result for key in 216 ['test_name', 'main_name']]), 217 msg='Unexpected keys in resulting dictionary: %s' % result) 218 self.assertEqual(result['main_name'], 'new_main_name', 219 msg='Unexpected "main_name" value: %s' % 220 result['main_name']) 221 self.assertEqual(result['test_name'], 'new_test_name', 222 msg='Unexpected "test_name" value: %s' % 223 result['test_name']) 224 225 def test_test_name_specified(self): 226 """Verifies gathers presentation info correctly.""" 227 result = perf_uploader._gather_presentation_info( 228 self._PRESENT_INFO, 'test_name') 229 self.assertTrue( 230 all([key in result for key in 231 ['test_name', 'main_name']]), 232 msg='Unexpected keys in resulting dictionary: %s' % result) 233 self.assertEqual(result['main_name'], 'new_main_name', 234 msg='Unexpected "main_name" value: %s' % 235 result['main_name']) 236 self.assertEqual(result['test_name'], 'new_test_name', 237 msg='Unexpected "test_name" value: %s' % 238 result['test_name']) 239 240 241 def test_test_name_not_specified(self): 242 """Verifies exception raised if test is not there.""" 243 self.assertRaises( 244 perf_uploader.PerfUploadingError, 245 perf_uploader._gather_presentation_info, 246 self._PRESENT_INFO, 'other_test_name') 247 248 249 def test_main_not_specified(self): 250 """Verifies exception raised if main is not there.""" 251 self.assertRaises( 252 perf_uploader.PerfUploadingError, 253 perf_uploader._gather_presentation_info, 254 self._PRESENT_INFO_MISSING_MAIN, 'test_name') 255 256 257class test_parse_and_gather_presentation(unittest.TestCase): 258 """Tests for _parse_config_file and then_gather_presentation_info.""" 259 _AUTOTEST_NAME_CONFIG = """[{ 260 "autotest_name": "test.test.VM", 261 "main_name": "ChromeOSPerf" 262 }]""" 263 264 _AUTOTEST_REGEX_CONFIG = r"""[{ 265 "autotest_regex": "test\\.test\\.VM.*", 266 "main_name": "ChromeOSPerf" 267 }]""" 268 269 def setUp(self): 270 _, self._temp_path = tempfile.mkstemp() 271 272 def tearDown(self): 273 os.remove(self._temp_path) 274 275 def test_autotest_name_is_matched(self): 276 """Verifies that autotest name is matched to the test properly.""" 277 with open(self._temp_path, 'w') as f: 278 f.write(self._AUTOTEST_NAME_CONFIG) 279 config = perf_uploader._parse_config_file(self._temp_path) 280 test_name = 'test.test.VM' 281 result = perf_uploader._gather_presentation_info(config, test_name) 282 self.assertEqual(result, { 283 'test_name': test_name, 284 'main_name': 'ChromeOSPerf' 285 }) 286 287 def test_autotest_name_is_exact_matched(self): 288 """Verifies that autotest name is exact matched to the test properly.""" 289 with open(self._temp_path, 'w') as f: 290 f.write(self._AUTOTEST_NAME_CONFIG) 291 config = perf_uploader._parse_config_file(self._temp_path) 292 test_name = 'test.test.VM.test' 293 try: 294 perf_uploader._gather_presentation_info(config, test_name) 295 self.fail( 296 'PerfUploadingError is expected for %s. autotest_name should ' 297 'be exactly matched.' % test_name) 298 except perf_uploader.PerfUploadingError: 299 return 300 301 def test_autotest_name_is_escaped(self): 302 """Verifies that autotest name is escaped properly.""" 303 with open(self._temp_path, 'w') as f: 304 f.write(self._AUTOTEST_NAME_CONFIG) 305 config = perf_uploader._parse_config_file(self._temp_path) 306 try: 307 test_name = 'test.testkVM' 308 result = perf_uploader._gather_presentation_info( 309 config, test_name) 310 self.fail( 311 'PerfUploadingError is expected for %s. autotest_name should ' 312 'be escaped' % test_name) 313 except perf_uploader.PerfUploadingError: 314 return 315 316 def test_autotest_regex_is_matched(self): 317 """Verifies that autotest regex is matched to the test properly.""" 318 with open(self._temp_path, 'w') as f: 319 f.write(self._AUTOTEST_REGEX_CONFIG) 320 config = perf_uploader._parse_config_file(self._temp_path) 321 for test_name in ['test.test.VM1', 'test.test.VMTest']: 322 result = perf_uploader._gather_presentation_info(config, test_name) 323 self.assertEqual(result, { 324 'test_name': test_name, 325 'main_name': 'ChromeOSPerf' 326 }) 327 328 def test_autotest_regex_is_not_matched(self): 329 """Verifies that autotest regex is matched to the test properly.""" 330 with open(self._temp_path, 'w') as f: 331 f.write(self._AUTOTEST_REGEX_CONFIG) 332 config = perf_uploader._parse_config_file(self._temp_path) 333 for test_name in ['testktest.VM', 'test.testkVM', 'test.test\VM']: 334 try: 335 result = perf_uploader._gather_presentation_info( 336 config, test_name) 337 self.fail('PerfUploadingError is expected for %s' % test_name) 338 except perf_uploader.PerfUploadingError: 339 return 340 341class test_get_id_from_version(unittest.TestCase): 342 """Tests for the _get_id_from_version function.""" 343 344 def test_correctly_formatted_versions(self): 345 """Verifies that the expected ID is returned when input is OK.""" 346 chrome_version = '27.0.1452.2' 347 cros_version = '27.3906.0.0' 348 # 1452.2 + 3906.0.0 349 # --> 01452 + 002 + 03906 + 000 + 00 350 # --> 14520020390600000 351 self.assertEqual( 352 14520020390600000, 353 perf_uploader._get_id_from_version( 354 chrome_version, cros_version)) 355 356 chrome_version = '25.10.1000.0' 357 cros_version = '25.1200.0.0' 358 # 1000.0 + 1200.0.0 359 # --> 01000 + 000 + 01200 + 000 + 00 360 # --> 10000000120000000 361 self.assertEqual( 362 10000000120000000, 363 perf_uploader._get_id_from_version( 364 chrome_version, cros_version)) 365 366 def test_returns_none_when_given_invalid_input(self): 367 """Checks the return value when invalid input is given.""" 368 chrome_version = '27.0' 369 cros_version = '27.3906.0.0' 370 self.assertIsNone(perf_uploader._get_id_from_version( 371 chrome_version, cros_version)) 372 373 374class test_get_version_numbers(unittest.TestCase): 375 """Tests for the _get_version_numbers function.""" 376 377 def test_with_valid_versions(self): 378 """Checks the version numbers used when data is formatted as expected.""" 379 self.assertEqual(('34.5678.9.0', '34.5.678.9'), 380 perf_uploader._get_version_numbers({ 381 'CHROME_VERSION': 382 '34.5.678.9', 383 'CHROMEOS_RELEASE_VERSION': 384 '5678.9.0', 385 })) 386 387 def test_with_missing_version_raises_error(self): 388 """Checks that an error is raised when a version is missing.""" 389 with self.assertRaises(perf_uploader.PerfUploadingError): 390 perf_uploader._get_version_numbers({ 391 'CHROMEOS_RELEASE_VERSION': 392 '5678.9.0', 393 }) 394 395 def test_with_unexpected_version_format_raises_error(self): 396 """Checks that an error is raised when there's a rN suffix.""" 397 with self.assertRaises(perf_uploader.PerfUploadingError): 398 perf_uploader._get_version_numbers({ 399 'CHROME_VERSION': 400 '34.5.678.9', 401 'CHROMEOS_RELEASE_VERSION': 402 '5678.9.0r1', 403 }) 404 405 def test_with_valid_release_milestone(self): 406 """Checks the version numbers used when data is formatted as expected.""" 407 self.assertEqual(('54.5678.9.0', '34.5.678.9'), 408 perf_uploader._get_version_numbers({ 409 'CHROME_VERSION': 410 '34.5.678.9', 411 'CHROMEOS_RELEASE_VERSION': 412 '5678.9.0', 413 'CHROMEOS_RELEASE_CHROME_MILESTONE': 414 '54', 415 })) 416 417 418class test_format_for_upload(unittest.TestCase): 419 """Tests for the format_for_upload function.""" 420 421 _PERF_DATA = { 422 "charts": { 423 "metric1": { 424 "summary": { 425 "improvement_direction": "down", 426 "type": "scalar", 427 "units": "msec", 428 "value": 2.7, 429 } 430 }, 431 "metric2": { 432 "summary": { 433 "improvement_direction": "up", 434 "type": "scalar", 435 "units": "frames_per_sec", 436 "value": 101.35, 437 } 438 } 439 }, 440 } 441 _PRESENT_INFO = { 442 'main_name': 'new_main_name', 443 'test_name': 'new_test_name', 444 } 445 446 def setUp(self): 447 self._perf_data = self._PERF_DATA 448 449 def _verify_result_string(self, actual_result, expected_result): 450 """Verifies a JSON string matches the expected result. 451 452 This function compares JSON objects rather than strings, because of 453 possible floating-point values that need to be compared using 454 assertAlmostEqual(). 455 456 @param actual_result: The candidate JSON string. 457 @param expected_result: The reference JSON string that the candidate 458 must match. 459 460 """ 461 actual = json.loads(actual_result) 462 expected = json.loads(expected_result) 463 464 def ordered(obj): 465 """Return the sorted obj.""" 466 if isinstance(obj, dict): 467 return sorted((k, ordered(v)) for k, v in obj.items()) 468 if isinstance(obj, list): 469 return sorted(ordered(x) for x in obj) 470 else: 471 return obj 472 473 fail_msg = 'Unexpected result string: %s' % actual_result 474 self.assertEqual(ordered(expected), ordered(actual), msg=fail_msg) 475 476 477 def test_format_for_upload(self): 478 """Verifies format_for_upload generates correct json data.""" 479 result = perf_uploader._format_for_upload( 480 'platform', '25.1200.0.0', '25.10.1000.0', 'WINKY E2A-F2K-Q35', 481 'test_machine', self._perf_data, self._PRESENT_INFO, 482 '52926644-username/hostname') 483 # TODO b:169251326 terms below are set outside of this codebase and 484 # should be updated when possible ("master" -> "main"). # nocheck 485 # see catapult-project/catapult/dashboard/dashboard/add_point.py 486 expected_result_string = ( 487 '{"versions": {' 488 '"cros_version": "25.1200.0.0",' 489 '"chrome_version": "25.10.1000.0"' 490 '},' 491 '"point_id": 10000000120000000,' 492 '"bot": "cros-platform",' 493 '"chart_data": {' 494 '"charts": {' 495 '"metric2": {' 496 '"summary": {' 497 '"units": "frames_per_sec",' 498 '"type": "scalar",' 499 '"value": 101.35,' 500 '"improvement_direction": "up"' 501 '}' 502 '},' 503 '"metric1": {' 504 '"summary": {' 505 '"units": "msec",' 506 '"type": "scalar",' 507 '"value": 2.7,' 508 '"improvement_direction": "down"}' 509 '}' 510 '}' 511 '},' 512 '"master": "new_main_name",' # nocheck 513 '"supplemental": {' 514 '"hardware_identifier": "WINKY E2A-F2K-Q35",' 515 '"jobname": "52926644-username/hostname",' 516 '"hardware_hostname": "test_machine",' 517 '"default_rev": "r_cros_version"}' 518 '}') 519 self._verify_result_string(result['data'], expected_result_string) 520 521 522if __name__ == '__main__': 523 unittest.main() 524