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