| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # |
| # Copyright 2014 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Tests for the experiment runner module.""" |
| |
| |
| import getpass |
| import io |
| import os |
| import time |
| import unittest |
| import unittest.mock as mock |
| |
| from cros_utils import command_executer |
| from cros_utils.email_sender import EmailSender |
| from cros_utils.file_utils import FileUtils |
| from experiment_factory import ExperimentFactory |
| from experiment_file import ExperimentFile |
| import experiment_runner |
| import experiment_status |
| import machine_manager |
| from results_cache import Result |
| from results_report import HTMLResultsReport |
| from results_report import TextResultsReport |
| import test_flag |
| |
| import config |
| |
| |
| EXPERIMENT_FILE_1 = """ |
| board: parrot |
| remote: chromeos-parrot1.cros chromreos-parrot2.cros |
| locks_dir: /tmp |
| |
| benchmark: kraken { |
| suite: telemetry_Crosperf |
| iterations: 3 |
| } |
| |
| image1 { |
| chromeos_root: /usr/local/google/chromeos |
| chromeos_image: /usr/local/google/chromeos/src/build/images/parrot/latest/cros_image1.bin |
| } |
| |
| image2 { |
| chromeos_image: /usr/local/google/chromeos/src/build/imaages/parrot/latest/cros_image2.bin |
| } |
| """ |
| |
| # pylint: disable=protected-access |
| |
| |
| class FakeLogger(object): |
| """Fake logger for tests.""" |
| |
| def __init__(self): |
| self.LogOutputCount = 0 |
| self.LogErrorCount = 0 |
| self.output_msgs = [] |
| self.error_msgs = [] |
| self.dot_count = 0 |
| self.LogStartDotsCount = 0 |
| self.LogEndDotsCount = 0 |
| self.LogAppendDotCount = 0 |
| |
| def LogOutput(self, msg): |
| self.LogOutputCount += 1 |
| self.output_msgs.append(msg) |
| |
| def LogError(self, msg): |
| self.LogErrorCount += 1 |
| self.error_msgs.append(msg) |
| |
| def LogStartDots(self): |
| self.LogStartDotsCount += 1 |
| self.dot_count += 1 |
| |
| def LogAppendDot(self): |
| self.LogAppendDotCount += 1 |
| self.dot_count += 1 |
| |
| def LogEndDots(self): |
| self.LogEndDotsCount += 1 |
| |
| def Reset(self): |
| self.LogOutputCount = 0 |
| self.LogErrorCount = 0 |
| self.output_msgs = [] |
| self.error_msgs = [] |
| self.dot_count = 0 |
| self.LogStartDotsCount = 0 |
| self.LogEndDotsCount = 0 |
| self.LogAppendDotCount = 0 |
| |
| |
| class ExperimentRunnerTest(unittest.TestCase): |
| """Test for experiment runner class.""" |
| |
| run_count = 0 |
| is_complete_count = 0 |
| mock_logger = FakeLogger() |
| mock_cmd_exec = mock.Mock(spec=command_executer.CommandExecuter) |
| |
| def make_fake_experiment(self): |
| test_flag.SetTestMode(True) |
| experiment_file = ExperimentFile(io.StringIO(EXPERIMENT_FILE_1)) |
| experiment = ExperimentFactory().GetExperiment( |
| experiment_file, working_directory="", log_dir="" |
| ) |
| return experiment |
| |
| @mock.patch.object(machine_manager.MachineManager, "AddMachine") |
| @mock.patch.object(os.path, "isfile") |
| |
| # pylint: disable=arguments-differ |
| def setUp(self, mock_isfile, _mock_addmachine): |
| mock_isfile.return_value = True |
| self.exp = self.make_fake_experiment() |
| |
| def test_init(self): |
| er = experiment_runner.ExperimentRunner( |
| self.exp, |
| json_report=False, |
| using_schedv2=False, |
| log=self.mock_logger, |
| cmd_exec=self.mock_cmd_exec, |
| ) |
| self.assertFalse(er._terminated) |
| self.assertEqual(er.STATUS_TIME_DELAY, 10) |
| |
| self.exp.log_level = "verbose" |
| er = experiment_runner.ExperimentRunner( |
| self.exp, |
| json_report=False, |
| using_schedv2=False, |
| log=self.mock_logger, |
| cmd_exec=self.mock_cmd_exec, |
| ) |
| self.assertEqual(er.STATUS_TIME_DELAY, 30) |
| |
| @mock.patch.object(time, "time") |
| @mock.patch.object(time, "sleep") |
| @mock.patch.object(experiment_status.ExperimentStatus, "GetStatusString") |
| @mock.patch.object(experiment_status.ExperimentStatus, "GetProgressString") |
| def test_run( |
| self, mock_progress_string, mock_status_string, mock_sleep, mock_time |
| ): |
| |
| self.run_count = 0 |
| self.is_complete_count = 0 |
| mock_sleep.return_value = None |
| # pylint: disable=range-builtin-not-iterating |
| mock_time.side_effect = range(1, 50, 1) |
| |
| def reset(): |
| self.run_count = 0 |
| self.is_complete_count = 0 |
| |
| def FakeRun(): |
| self.run_count += 1 |
| return 0 |
| |
| def FakeIsComplete(): |
| self.is_complete_count += 1 |
| if self.is_complete_count < 6: |
| return False |
| else: |
| return True |
| |
| self.mock_logger.Reset() |
| self.exp.Run = FakeRun |
| self.exp.IsComplete = FakeIsComplete |
| |
| # Test 1: log_level == "quiet" |
| self.exp.log_level = "quiet" |
| er = experiment_runner.ExperimentRunner( |
| self.exp, |
| json_report=False, |
| using_schedv2=False, |
| log=self.mock_logger, |
| cmd_exec=self.mock_cmd_exec, |
| ) |
| er.STATUS_TIME_DELAY = 2 |
| mock_status_string.return_value = "Fake status string" |
| er._Run(self.exp) |
| self.assertEqual(self.run_count, 1) |
| self.assertTrue(self.is_complete_count > 0) |
| self.assertEqual(self.mock_logger.LogStartDotsCount, 1) |
| self.assertEqual(self.mock_logger.LogAppendDotCount, 1) |
| self.assertEqual(self.mock_logger.LogEndDotsCount, 1) |
| self.assertEqual(self.mock_logger.dot_count, 2) |
| self.assertEqual(mock_progress_string.call_count, 0) |
| self.assertEqual(mock_status_string.call_count, 2) |
| self.assertEqual( |
| self.mock_logger.output_msgs, |
| [ |
| "==============================", |
| "Fake status string", |
| "==============================", |
| ], |
| ) |
| self.assertEqual(len(self.mock_logger.error_msgs), 0) |
| |
| # Test 2: log_level == "average" |
| self.mock_logger.Reset() |
| reset() |
| self.exp.log_level = "average" |
| mock_status_string.call_count = 0 |
| er = experiment_runner.ExperimentRunner( |
| self.exp, |
| json_report=False, |
| using_schedv2=False, |
| log=self.mock_logger, |
| cmd_exec=self.mock_cmd_exec, |
| ) |
| er.STATUS_TIME_DELAY = 2 |
| mock_status_string.return_value = "Fake status string" |
| er._Run(self.exp) |
| self.assertEqual(self.run_count, 1) |
| self.assertTrue(self.is_complete_count > 0) |
| self.assertEqual(self.mock_logger.LogStartDotsCount, 1) |
| self.assertEqual(self.mock_logger.LogAppendDotCount, 1) |
| self.assertEqual(self.mock_logger.LogEndDotsCount, 1) |
| self.assertEqual(self.mock_logger.dot_count, 2) |
| self.assertEqual(mock_progress_string.call_count, 0) |
| self.assertEqual(mock_status_string.call_count, 2) |
| self.assertEqual( |
| self.mock_logger.output_msgs, |
| [ |
| "==============================", |
| "Fake status string", |
| "==============================", |
| ], |
| ) |
| self.assertEqual(len(self.mock_logger.error_msgs), 0) |
| |
| # Test 3: log_level == "verbose" |
| self.mock_logger.Reset() |
| reset() |
| self.exp.log_level = "verbose" |
| mock_status_string.call_count = 0 |
| er = experiment_runner.ExperimentRunner( |
| self.exp, |
| json_report=False, |
| using_schedv2=False, |
| log=self.mock_logger, |
| cmd_exec=self.mock_cmd_exec, |
| ) |
| er.STATUS_TIME_DELAY = 2 |
| mock_status_string.return_value = "Fake status string" |
| mock_progress_string.return_value = "Fake progress string" |
| er._Run(self.exp) |
| self.assertEqual(self.run_count, 1) |
| self.assertTrue(self.is_complete_count > 0) |
| self.assertEqual(self.mock_logger.LogStartDotsCount, 0) |
| self.assertEqual(self.mock_logger.LogAppendDotCount, 0) |
| self.assertEqual(self.mock_logger.LogEndDotsCount, 0) |
| self.assertEqual(self.mock_logger.dot_count, 0) |
| self.assertEqual(mock_progress_string.call_count, 2) |
| self.assertEqual(mock_status_string.call_count, 2) |
| self.assertEqual( |
| self.mock_logger.output_msgs, |
| [ |
| "==============================", |
| "Fake progress string", |
| "Fake status string", |
| "==============================", |
| "==============================", |
| "Fake progress string", |
| "Fake status string", |
| "==============================", |
| ], |
| ) |
| self.assertEqual(len(self.mock_logger.error_msgs), 0) |
| |
| @mock.patch.object(TextResultsReport, "GetReport") |
| def test_print_table(self, mock_report): |
| self.mock_logger.Reset() |
| mock_report.return_value = "This is a fake experiment report." |
| er = experiment_runner.ExperimentRunner( |
| self.exp, |
| json_report=False, |
| using_schedv2=False, |
| log=self.mock_logger, |
| cmd_exec=self.mock_cmd_exec, |
| ) |
| er._PrintTable(self.exp) |
| self.assertEqual(mock_report.call_count, 1) |
| self.assertEqual( |
| self.mock_logger.output_msgs, ["This is a fake experiment report."] |
| ) |
| |
| @mock.patch.object(HTMLResultsReport, "GetReport") |
| @mock.patch.object(TextResultsReport, "GetReport") |
| @mock.patch.object(EmailSender, "Attachment") |
| @mock.patch.object(EmailSender, "SendEmail") |
| @mock.patch.object(getpass, "getuser") |
| def test_email( |
| self, |
| mock_getuser, |
| mock_emailer, |
| mock_attachment, |
| mock_text_report, |
| mock_html_report, |
| ): |
| |
| mock_getuser.return_value = "john.smith@google.com" |
| mock_text_report.return_value = "This is a fake text report." |
| mock_html_report.return_value = "This is a fake html report." |
| |
| self.mock_logger.Reset() |
| config.AddConfig("no_email", True) |
| self.exp.email_to = ["jane.doe@google.com"] |
| er = experiment_runner.ExperimentRunner( |
| self.exp, |
| json_report=False, |
| using_schedv2=False, |
| log=self.mock_logger, |
| cmd_exec=self.mock_cmd_exec, |
| ) |
| # Test 1. Config:no_email; exp.email_to set ==> no email sent |
| er._Email(self.exp) |
| self.assertEqual(mock_getuser.call_count, 0) |
| self.assertEqual(mock_emailer.call_count, 0) |
| self.assertEqual(mock_attachment.call_count, 0) |
| self.assertEqual(mock_text_report.call_count, 0) |
| self.assertEqual(mock_html_report.call_count, 0) |
| |
| # Test 2. Config: email. exp.email_to set; cache hit. => send email |
| self.mock_logger.Reset() |
| config.AddConfig("no_email", False) |
| for r in self.exp.benchmark_runs: |
| r.cache_hit = True |
| er._Email(self.exp) |
| self.assertEqual(mock_getuser.call_count, 1) |
| self.assertEqual(mock_emailer.call_count, 1) |
| self.assertEqual(mock_attachment.call_count, 1) |
| self.assertEqual(mock_text_report.call_count, 1) |
| self.assertEqual(mock_html_report.call_count, 1) |
| self.assertEqual(len(mock_emailer.call_args), 2) |
| self.assertEqual( |
| mock_emailer.call_args[0], |
| ( |
| ["jane.doe@google.com", "john.smith@google.com"], |
| ": image1 vs. image2", |
| "<pre style='font-size: 13px'>This is a fake text " |
| "report.\nResults are stored in _results.\n</pre>", |
| ), |
| ) |
| self.assertTrue(isinstance(mock_emailer.call_args[1], dict)) |
| self.assertEqual(len(mock_emailer.call_args[1]), 2) |
| self.assertTrue("attachments" in mock_emailer.call_args[1].keys()) |
| self.assertEqual(mock_emailer.call_args[1]["msg_type"], "html") |
| |
| mock_attachment.assert_called_with( |
| "report.html", "This is a fake html report." |
| ) |
| |
| # Test 3. Config: email; exp.mail_to set; no cache hit. => send email |
| self.mock_logger.Reset() |
| mock_getuser.reset_mock() |
| mock_emailer.reset_mock() |
| mock_attachment.reset_mock() |
| mock_text_report.reset_mock() |
| mock_html_report.reset_mock() |
| config.AddConfig("no_email", False) |
| for r in self.exp.benchmark_runs: |
| r.cache_hit = False |
| er._Email(self.exp) |
| self.assertEqual(mock_getuser.call_count, 1) |
| self.assertEqual(mock_emailer.call_count, 1) |
| self.assertEqual(mock_attachment.call_count, 1) |
| self.assertEqual(mock_text_report.call_count, 1) |
| self.assertEqual(mock_html_report.call_count, 1) |
| self.assertEqual(len(mock_emailer.call_args), 2) |
| self.assertEqual( |
| mock_emailer.call_args[0], |
| ( |
| [ |
| "jane.doe@google.com", |
| "john.smith@google.com", |
| "john.smith@google.com", |
| ], |
| ": image1 vs. image2", |
| "<pre style='font-size: 13px'>This is a fake text " |
| "report.\nResults are stored in _results.\n</pre>", |
| ), |
| ) |
| self.assertTrue(isinstance(mock_emailer.call_args[1], dict)) |
| self.assertEqual(len(mock_emailer.call_args[1]), 2) |
| self.assertTrue("attachments" in mock_emailer.call_args[1].keys()) |
| self.assertEqual(mock_emailer.call_args[1]["msg_type"], "html") |
| |
| mock_attachment.assert_called_with( |
| "report.html", "This is a fake html report." |
| ) |
| |
| # Test 4. Config: email; exp.mail_to = None; no cache hit. => send email |
| self.mock_logger.Reset() |
| mock_getuser.reset_mock() |
| mock_emailer.reset_mock() |
| mock_attachment.reset_mock() |
| mock_text_report.reset_mock() |
| mock_html_report.reset_mock() |
| self.exp.email_to = [] |
| er._Email(self.exp) |
| self.assertEqual(mock_getuser.call_count, 1) |
| self.assertEqual(mock_emailer.call_count, 1) |
| self.assertEqual(mock_attachment.call_count, 1) |
| self.assertEqual(mock_text_report.call_count, 1) |
| self.assertEqual(mock_html_report.call_count, 1) |
| self.assertEqual(len(mock_emailer.call_args), 2) |
| self.assertEqual( |
| mock_emailer.call_args[0], |
| ( |
| ["john.smith@google.com"], |
| ": image1 vs. image2", |
| "<pre style='font-size: 13px'>This is a fake text " |
| "report.\nResults are stored in _results.\n</pre>", |
| ), |
| ) |
| self.assertTrue(isinstance(mock_emailer.call_args[1], dict)) |
| self.assertEqual(len(mock_emailer.call_args[1]), 2) |
| self.assertTrue("attachments" in mock_emailer.call_args[1].keys()) |
| self.assertEqual(mock_emailer.call_args[1]["msg_type"], "html") |
| |
| mock_attachment.assert_called_with( |
| "report.html", "This is a fake html report." |
| ) |
| |
| # Test 5. Config: email; exp.mail_to = None; cache hit => no email sent |
| self.mock_logger.Reset() |
| mock_getuser.reset_mock() |
| mock_emailer.reset_mock() |
| mock_attachment.reset_mock() |
| mock_text_report.reset_mock() |
| mock_html_report.reset_mock() |
| for r in self.exp.benchmark_runs: |
| r.cache_hit = True |
| er._Email(self.exp) |
| self.assertEqual(mock_getuser.call_count, 0) |
| self.assertEqual(mock_emailer.call_count, 0) |
| self.assertEqual(mock_attachment.call_count, 0) |
| self.assertEqual(mock_text_report.call_count, 0) |
| self.assertEqual(mock_html_report.call_count, 0) |
| |
| @mock.patch.object(FileUtils, "RmDir") |
| @mock.patch.object(FileUtils, "MkDirP") |
| @mock.patch.object(FileUtils, "WriteFile") |
| @mock.patch.object(HTMLResultsReport, "FromExperiment") |
| @mock.patch.object(TextResultsReport, "FromExperiment") |
| @mock.patch.object(Result, "CompressResultsTo") |
| @mock.patch.object(Result, "CopyResultsTo") |
| @mock.patch.object(Result, "CleanUp") |
| @mock.patch.object(Result, "FormatStringTopCommands") |
| @mock.patch("builtins.open", new_callable=mock.mock_open) |
| def test_store_results( |
| self, |
| mock_open, |
| mock_top_commands, |
| mock_cleanup, |
| mock_copy, |
| mock_compress, |
| _mock_text_report, |
| mock_report, |
| mock_writefile, |
| mock_mkdir, |
| mock_rmdir, |
| ): |
| |
| self.mock_logger.Reset() |
| self.exp.results_directory = "/usr/local/crosperf-results" |
| bench_run = self.exp.benchmark_runs[5] |
| bench_path = "/usr/local/crosperf-results/" + "".join( |
| ch for ch in bench_run.name if ch.isalnum() |
| ) |
| self.assertEqual(len(self.exp.benchmark_runs), 6) |
| |
| er = experiment_runner.ExperimentRunner( |
| self.exp, |
| json_report=False, |
| using_schedv2=False, |
| log=self.mock_logger, |
| cmd_exec=self.mock_cmd_exec, |
| ) |
| |
| # Test 1. Make sure nothing is done if _terminated is true. |
| er._terminated = True |
| er._StoreResults(self.exp) |
| self.assertEqual(mock_cleanup.call_count, 0) |
| self.assertEqual(mock_copy.call_count, 0) |
| self.assertEqual(mock_compress.call_count, 0) |
| self.assertEqual(mock_report.call_count, 0) |
| self.assertEqual(mock_writefile.call_count, 0) |
| self.assertEqual(mock_mkdir.call_count, 0) |
| self.assertEqual(mock_rmdir.call_count, 0) |
| self.assertEqual(self.mock_logger.LogOutputCount, 0) |
| self.assertEqual(mock_open.call_count, 0) |
| self.assertEqual(mock_top_commands.call_count, 0) |
| |
| # Test 2. _terminated is false; everything works properly. |
| fake_result = Result( |
| self.mock_logger, self.exp.labels[0], "average", "daisy1" |
| ) |
| for r in self.exp.benchmark_runs: |
| r.result = fake_result |
| er._terminated = False |
| self.exp.compress_results = False |
| er._StoreResults(self.exp) |
| self.assertEqual(mock_cleanup.call_count, 6) |
| mock_cleanup.assert_called_with(bench_run.benchmark.rm_chroot_tmp) |
| self.assertEqual(mock_copy.call_count, 6) |
| mock_copy.assert_called_with(bench_path) |
| self.assertEqual(mock_writefile.call_count, 3) |
| self.assertEqual(len(mock_writefile.call_args_list), 3) |
| first_args = mock_writefile.call_args_list[0] |
| second_args = mock_writefile.call_args_list[1] |
| self.assertEqual( |
| first_args[0][0], "/usr/local/crosperf-results/experiment.exp" |
| ) |
| self.assertEqual( |
| second_args[0][0], "/usr/local/crosperf-results/results.html" |
| ) |
| self.assertEqual(mock_mkdir.call_count, 1) |
| mock_mkdir.assert_called_with("/usr/local/crosperf-results") |
| self.assertEqual(mock_rmdir.call_count, 1) |
| mock_rmdir.assert_called_with("/usr/local/crosperf-results") |
| self.assertEqual(self.mock_logger.LogOutputCount, 5) |
| self.assertEqual( |
| self.mock_logger.output_msgs, |
| [ |
| "Storing experiment file in /usr/local/crosperf-results.", |
| "Storing top statistics of each benchmark run into" |
| " /usr/local/crosperf-results/topstats.log.", |
| "Storing results of each benchmark run.", |
| "Storing results report in /usr/local/crosperf-results.", |
| "Storing email message body in /usr/local/crosperf-results.", |
| ], |
| ) |
| self.assertEqual(mock_open.call_count, 1) |
| # Check write to a topstats.log file. |
| mock_open.assert_called_with( |
| "/usr/local/crosperf-results/topstats.log", "w" |
| ) |
| mock_open().write.assert_called() |
| |
| # Check top calls with no arguments. |
| topcalls = [mock.call()] * 6 |
| self.assertEqual(mock_top_commands.call_args_list, topcalls) |
| |
| # Test 3. Test compress_results. |
| self.exp.compress_results = True |
| mock_copy.call_count = 0 |
| mock_compress.call_count = 0 |
| er._StoreResults(self.exp) |
| self.assertEqual(mock_copy.call_count, 0) |
| mock_copy.assert_called_with(bench_path) |
| self.assertEqual(mock_compress.call_count, 6) |
| mock_compress.assert_called_with(bench_path) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |