1 #!/usr/bin/env python2 2 # 3 # Copyright (c) 2014 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 """Tests for the experiment runner module.""" 7 8 from __future__ import print_function 9 10 import StringIO 11 import getpass 12 import os 13 14 import mock 15 import unittest 16 17 import experiment_runner 18 import experiment_status 19 import machine_manager 20 import config 21 import test_flag 22 23 from experiment_factory import ExperimentFactory 24 from experiment_file import ExperimentFile 25 from results_cache import Result 26 from results_report import HTMLResultsReport 27 from results_report import TextResultsReport 28 29 from cros_utils import command_executer 30 from cros_utils.email_sender import EmailSender 31 from cros_utils.file_utils import FileUtils 32 33 EXPERIMENT_FILE_1 = """ 34 board: parrot 35 remote: chromeos-parrot1.cros chromreos-parrot2.cros 36 37 benchmark: kraken { 38 suite: telemetry_Crosperf 39 iterations: 3 40 } 41 42 image1 { 43 chromeos_root: /usr/local/google/chromeos 44 chromeos_image: /usr/local/google/chromeos/src/build/images/parrot/latest/cros_image1.bin 45 } 46 47 image2 { 48 chromeos_image: /usr/local/google/chromeos/src/build/imaages/parrot/latest/cros_image2.bin 49 } 50 """ 51 52 # pylint: disable=protected-access 53 54 55 class FakeLogger(object): 56 """Fake logger for tests.""" 57 58 def __init__(self): 59 self.LogOutputCount = 0 60 self.LogErrorCount = 0 61 self.output_msgs = [] 62 self.error_msgs = [] 63 self.dot_count = 0 64 self.LogStartDotsCount = 0 65 self.LogEndDotsCount = 0 66 self.LogAppendDotCount = 0 67 68 def LogOutput(self, msg): 69 self.LogOutputCount += 1 70 self.output_msgs.append(msg) 71 72 def LogError(self, msg): 73 self.LogErrorCount += 1 74 self.error_msgs.append(msg) 75 76 def LogStartDots(self): 77 self.LogStartDotsCount += 1 78 self.dot_count += 1 79 80 def LogAppendDot(self): 81 self.LogAppendDotCount += 1 82 self.dot_count += 1 83 84 def LogEndDots(self): 85 self.LogEndDotsCount += 1 86 87 def Reset(self): 88 self.LogOutputCount = 0 89 self.LogErrorCount = 0 90 self.output_msgs = [] 91 self.error_msgs = [] 92 self.dot_count = 0 93 self.LogStartDotsCount = 0 94 self.LogEndDotsCount = 0 95 self.LogAppendDotCount = 0 96 97 98 class ExperimentRunnerTest(unittest.TestCase): 99 """Test for experiment runner class.""" 100 101 run_count = 0 102 is_complete_count = 0 103 mock_logger = FakeLogger() 104 mock_cmd_exec = mock.Mock(spec=command_executer.CommandExecuter) 105 106 def make_fake_experiment(self): 107 test_flag.SetTestMode(True) 108 experiment_file = ExperimentFile(StringIO.StringIO(EXPERIMENT_FILE_1)) 109 experiment = ExperimentFactory().GetExperiment(experiment_file, 110 working_directory='', 111 log_dir='') 112 return experiment 113 114 @mock.patch.object(machine_manager.MachineManager, 'AddMachine') 115 @mock.patch.object(os.path, 'isfile') 116 117 # pylint: disable=arguments-differ 118 def setUp(self, mock_isfile, _mock_addmachine): 119 mock_isfile.return_value = True 120 self.exp = self.make_fake_experiment() 121 122 def test_init(self): 123 er = experiment_runner.ExperimentRunner(self.exp, 124 json_report=False, 125 using_schedv2=False, 126 log=self.mock_logger, 127 cmd_exec=self.mock_cmd_exec) 128 self.assertFalse(er._terminated) 129 self.assertEqual(er.STATUS_TIME_DELAY, 10) 130 131 self.exp.log_level = 'verbose' 132 er = experiment_runner.ExperimentRunner(self.exp, 133 json_report=False, 134 using_schedv2=False, 135 log=self.mock_logger, 136 cmd_exec=self.mock_cmd_exec) 137 self.assertEqual(er.STATUS_TIME_DELAY, 30) 138 139 @mock.patch.object(experiment_status.ExperimentStatus, 'GetStatusString') 140 @mock.patch.object(experiment_status.ExperimentStatus, 'GetProgressString') 141 def test_run(self, mock_progress_string, mock_status_string): 142 143 self.run_count = 0 144 self.is_complete_count = 0 145 146 def reset(): 147 self.run_count = 0 148 self.is_complete_count = 0 149 150 def FakeRun(): 151 self.run_count += 1 152 return 0 153 154 def FakeIsComplete(): 155 self.is_complete_count += 1 156 if self.is_complete_count < 3: 157 return False 158 else: 159 return True 160 161 self.mock_logger.Reset() 162 self.exp.Run = FakeRun 163 self.exp.IsComplete = FakeIsComplete 164 165 # Test 1: log_level == "quiet" 166 self.exp.log_level = 'quiet' 167 er = experiment_runner.ExperimentRunner(self.exp, 168 json_report=False, 169 using_schedv2=False, 170 log=self.mock_logger, 171 cmd_exec=self.mock_cmd_exec) 172 er.STATUS_TIME_DELAY = 2 173 mock_status_string.return_value = 'Fake status string' 174 er._Run(self.exp) 175 self.assertEqual(self.run_count, 1) 176 self.assertTrue(self.is_complete_count > 0) 177 self.assertEqual(self.mock_logger.LogStartDotsCount, 1) 178 self.assertEqual(self.mock_logger.LogAppendDotCount, 1) 179 self.assertEqual(self.mock_logger.LogEndDotsCount, 1) 180 self.assertEqual(self.mock_logger.dot_count, 2) 181 self.assertEqual(mock_progress_string.call_count, 0) 182 self.assertEqual(mock_status_string.call_count, 2) 183 self.assertEqual(self.mock_logger.output_msgs, 184 ['==============================', 'Fake status string', 185 '==============================']) 186 self.assertEqual(len(self.mock_logger.error_msgs), 0) 187 188 # Test 2: log_level == "average" 189 self.mock_logger.Reset() 190 reset() 191 self.exp.log_level = 'average' 192 mock_status_string.call_count = 0 193 er = experiment_runner.ExperimentRunner(self.exp, 194 json_report=False, 195 using_schedv2=False, 196 log=self.mock_logger, 197 cmd_exec=self.mock_cmd_exec) 198 er.STATUS_TIME_DELAY = 2 199 mock_status_string.return_value = 'Fake status string' 200 er._Run(self.exp) 201 self.assertEqual(self.run_count, 1) 202 self.assertTrue(self.is_complete_count > 0) 203 self.assertEqual(self.mock_logger.LogStartDotsCount, 1) 204 self.assertEqual(self.mock_logger.LogAppendDotCount, 1) 205 self.assertEqual(self.mock_logger.LogEndDotsCount, 1) 206 self.assertEqual(self.mock_logger.dot_count, 2) 207 self.assertEqual(mock_progress_string.call_count, 0) 208 self.assertEqual(mock_status_string.call_count, 2) 209 self.assertEqual(self.mock_logger.output_msgs, 210 ['==============================', 'Fake status string', 211 '==============================']) 212 self.assertEqual(len(self.mock_logger.error_msgs), 0) 213 214 # Test 3: log_level == "verbose" 215 self.mock_logger.Reset() 216 reset() 217 self.exp.log_level = 'verbose' 218 mock_status_string.call_count = 0 219 er = experiment_runner.ExperimentRunner(self.exp, 220 json_report=False, 221 using_schedv2=False, 222 log=self.mock_logger, 223 cmd_exec=self.mock_cmd_exec) 224 er.STATUS_TIME_DELAY = 2 225 mock_status_string.return_value = 'Fake status string' 226 mock_progress_string.return_value = 'Fake progress string' 227 er._Run(self.exp) 228 self.assertEqual(self.run_count, 1) 229 self.assertTrue(self.is_complete_count > 0) 230 self.assertEqual(self.mock_logger.LogStartDotsCount, 0) 231 self.assertEqual(self.mock_logger.LogAppendDotCount, 0) 232 self.assertEqual(self.mock_logger.LogEndDotsCount, 0) 233 self.assertEqual(self.mock_logger.dot_count, 0) 234 self.assertEqual(mock_progress_string.call_count, 2) 235 self.assertEqual(mock_status_string.call_count, 2) 236 self.assertEqual(self.mock_logger.output_msgs, 237 ['==============================', 'Fake progress string', 238 'Fake status string', '==============================', 239 '==============================', 'Fake progress string', 240 'Fake status string', '==============================']) 241 self.assertEqual(len(self.mock_logger.error_msgs), 0) 242 243 @mock.patch.object(TextResultsReport, 'GetReport') 244 def test_print_table(self, mock_report): 245 self.mock_logger.Reset() 246 mock_report.return_value = 'This is a fake experiment report.' 247 er = experiment_runner.ExperimentRunner(self.exp, 248 json_report=False, 249 using_schedv2=False, 250 log=self.mock_logger, 251 cmd_exec=self.mock_cmd_exec) 252 er._PrintTable(self.exp) 253 self.assertEqual(mock_report.call_count, 1) 254 self.assertEqual(self.mock_logger.output_msgs, 255 ['This is a fake experiment report.']) 256 257 @mock.patch.object(HTMLResultsReport, 'GetReport') 258 @mock.patch.object(TextResultsReport, 'GetReport') 259 @mock.patch.object(EmailSender, 'Attachment') 260 @mock.patch.object(EmailSender, 'SendEmail') 261 @mock.patch.object(getpass, 'getuser') 262 def test_email(self, mock_getuser, mock_emailer, mock_attachment, 263 mock_text_report, mock_html_report): 264 265 mock_getuser.return_value = 'john.smith (at] google.com' 266 mock_text_report.return_value = 'This is a fake text report.' 267 mock_html_report.return_value = 'This is a fake html report.' 268 269 self.mock_logger.Reset() 270 config.AddConfig('no_email', True) 271 self.exp.email_to = ['jane.doe (at] google.com'] 272 er = experiment_runner.ExperimentRunner(self.exp, 273 json_report=False, 274 using_schedv2=False, 275 log=self.mock_logger, 276 cmd_exec=self.mock_cmd_exec) 277 # Test 1. Config:no_email; exp.email_to set ==> no email sent 278 er._Email(self.exp) 279 self.assertEqual(mock_getuser.call_count, 0) 280 self.assertEqual(mock_emailer.call_count, 0) 281 self.assertEqual(mock_attachment.call_count, 0) 282 self.assertEqual(mock_text_report.call_count, 0) 283 self.assertEqual(mock_html_report.call_count, 0) 284 285 # Test 2. Config: email. exp.email_to set; cache hit. => send email 286 self.mock_logger.Reset() 287 config.AddConfig('no_email', False) 288 for r in self.exp.benchmark_runs: 289 r.cache_hit = True 290 er._Email(self.exp) 291 self.assertEqual(mock_getuser.call_count, 1) 292 self.assertEqual(mock_emailer.call_count, 1) 293 self.assertEqual(mock_attachment.call_count, 1) 294 self.assertEqual(mock_text_report.call_count, 1) 295 self.assertEqual(mock_html_report.call_count, 1) 296 self.assertEqual(len(mock_emailer.call_args), 2) 297 self.assertEqual(mock_emailer.call_args[0], 298 (['jane.doe (at] google.com', 'john.smith (at] google.com'], 299 ': image1 vs. image2', 300 "<pre style='font-size: 13px'>This is a fake text " 301 'report.\nResults are stored in _results.\n</pre>')) 302 self.assertTrue(type(mock_emailer.call_args[1]) is dict) 303 self.assertEqual(len(mock_emailer.call_args[1]), 2) 304 self.assertTrue('attachments' in mock_emailer.call_args[1].keys()) 305 self.assertEqual(mock_emailer.call_args[1]['msg_type'], 'html') 306 307 mock_attachment.assert_called_with('report.html', 308 'This is a fake html report.') 309 310 # Test 3. Config: email; exp.mail_to set; no cache hit. => send email 311 self.mock_logger.Reset() 312 mock_getuser.reset_mock() 313 mock_emailer.reset_mock() 314 mock_attachment.reset_mock() 315 mock_text_report.reset_mock() 316 mock_html_report.reset_mock() 317 config.AddConfig('no_email', False) 318 for r in self.exp.benchmark_runs: 319 r.cache_hit = False 320 er._Email(self.exp) 321 self.assertEqual(mock_getuser.call_count, 1) 322 self.assertEqual(mock_emailer.call_count, 1) 323 self.assertEqual(mock_attachment.call_count, 1) 324 self.assertEqual(mock_text_report.call_count, 1) 325 self.assertEqual(mock_html_report.call_count, 1) 326 self.assertEqual(len(mock_emailer.call_args), 2) 327 self.assertEqual(mock_emailer.call_args[0], 328 (['jane.doe (at] google.com', 'john.smith (at] google.com', 329 'john.smith (at] google.com'], ': image1 vs. image2', 330 "<pre style='font-size: 13px'>This is a fake text " 331 'report.\nResults are stored in _results.\n</pre>')) 332 self.assertTrue(type(mock_emailer.call_args[1]) is dict) 333 self.assertEqual(len(mock_emailer.call_args[1]), 2) 334 self.assertTrue('attachments' in mock_emailer.call_args[1].keys()) 335 self.assertEqual(mock_emailer.call_args[1]['msg_type'], 'html') 336 337 mock_attachment.assert_called_with('report.html', 338 'This is a fake html report.') 339 340 # Test 4. Config: email; exp.mail_to = None; no cache hit. => send email 341 self.mock_logger.Reset() 342 mock_getuser.reset_mock() 343 mock_emailer.reset_mock() 344 mock_attachment.reset_mock() 345 mock_text_report.reset_mock() 346 mock_html_report.reset_mock() 347 self.exp.email_to = [] 348 er._Email(self.exp) 349 self.assertEqual(mock_getuser.call_count, 1) 350 self.assertEqual(mock_emailer.call_count, 1) 351 self.assertEqual(mock_attachment.call_count, 1) 352 self.assertEqual(mock_text_report.call_count, 1) 353 self.assertEqual(mock_html_report.call_count, 1) 354 self.assertEqual(len(mock_emailer.call_args), 2) 355 self.assertEqual(mock_emailer.call_args[0], 356 (['john.smith (at] google.com'], ': image1 vs. image2', 357 "<pre style='font-size: 13px'>This is a fake text " 358 'report.\nResults are stored in _results.\n</pre>')) 359 self.assertTrue(type(mock_emailer.call_args[1]) is dict) 360 self.assertEqual(len(mock_emailer.call_args[1]), 2) 361 self.assertTrue('attachments' in mock_emailer.call_args[1].keys()) 362 self.assertEqual(mock_emailer.call_args[1]['msg_type'], 'html') 363 364 mock_attachment.assert_called_with('report.html', 365 'This is a fake html report.') 366 367 # Test 5. Config: email; exp.mail_to = None; cache hit => no email sent 368 self.mock_logger.Reset() 369 mock_getuser.reset_mock() 370 mock_emailer.reset_mock() 371 mock_attachment.reset_mock() 372 mock_text_report.reset_mock() 373 mock_html_report.reset_mock() 374 for r in self.exp.benchmark_runs: 375 r.cache_hit = True 376 er._Email(self.exp) 377 self.assertEqual(mock_getuser.call_count, 0) 378 self.assertEqual(mock_emailer.call_count, 0) 379 self.assertEqual(mock_attachment.call_count, 0) 380 self.assertEqual(mock_text_report.call_count, 0) 381 self.assertEqual(mock_html_report.call_count, 0) 382 383 @mock.patch.object(FileUtils, 'RmDir') 384 @mock.patch.object(FileUtils, 'MkDirP') 385 @mock.patch.object(FileUtils, 'WriteFile') 386 @mock.patch.object(HTMLResultsReport, 'FromExperiment') 387 @mock.patch.object(TextResultsReport, 'FromExperiment') 388 @mock.patch.object(Result, 'CopyResultsTo') 389 @mock.patch.object(Result, 'CleanUp') 390 def test_store_results(self, mock_cleanup, mock_copy, _mock_text_report, 391 mock_report, mock_writefile, mock_mkdir, mock_rmdir): 392 393 self.mock_logger.Reset() 394 self.exp.results_directory = '/usr/local/crosperf-results' 395 bench_run = self.exp.benchmark_runs[5] 396 bench_path = '/usr/local/crosperf-results/' + filter(str.isalnum, 397 bench_run.name) 398 self.assertEqual(len(self.exp.benchmark_runs), 6) 399 400 er = experiment_runner.ExperimentRunner(self.exp, 401 json_report=False, 402 using_schedv2=False, 403 log=self.mock_logger, 404 cmd_exec=self.mock_cmd_exec) 405 406 # Test 1. Make sure nothing is done if _terminated is true. 407 er._terminated = True 408 er._StoreResults(self.exp) 409 self.assertEqual(mock_cleanup.call_count, 0) 410 self.assertEqual(mock_copy.call_count, 0) 411 self.assertEqual(mock_report.call_count, 0) 412 self.assertEqual(mock_writefile.call_count, 0) 413 self.assertEqual(mock_mkdir.call_count, 0) 414 self.assertEqual(mock_rmdir.call_count, 0) 415 self.assertEqual(self.mock_logger.LogOutputCount, 0) 416 417 # Test 2. _terminated is false; everything works properly. 418 fake_result = Result(self.mock_logger, self.exp.labels[0], 'average', 419 'daisy1') 420 for r in self.exp.benchmark_runs: 421 r.result = fake_result 422 er._terminated = False 423 er._StoreResults(self.exp) 424 self.assertEqual(mock_cleanup.call_count, 6) 425 mock_cleanup.called_with(bench_run.benchmark.rm_chroot_tmp) 426 self.assertEqual(mock_copy.call_count, 6) 427 mock_copy.called_with(bench_path) 428 self.assertEqual(mock_writefile.call_count, 3) 429 self.assertEqual(len(mock_writefile.call_args_list), 3) 430 first_args = mock_writefile.call_args_list[0] 431 second_args = mock_writefile.call_args_list[1] 432 self.assertEqual(first_args[0][0], 433 '/usr/local/crosperf-results/experiment.exp') 434 self.assertEqual(second_args[0][0], 435 '/usr/local/crosperf-results/results.html') 436 self.assertEqual(mock_mkdir.call_count, 1) 437 mock_mkdir.called_with('/usr/local/crosperf-results') 438 self.assertEqual(mock_rmdir.call_count, 1) 439 mock_rmdir.called_with('/usr/local/crosperf-results') 440 self.assertEqual(self.mock_logger.LogOutputCount, 4) 441 self.assertEqual( 442 self.mock_logger.output_msgs, 443 ['Storing experiment file in /usr/local/crosperf-results.', 444 'Storing results report in /usr/local/crosperf-results.', 445 'Storing email message body in /usr/local/crosperf-results.', 446 'Storing results of each benchmark run.']) 447 448 449 if __name__ == '__main__': 450 unittest.main() 451