Satya Tangirala | bbcc1c9 | 2017-07-18 11:46:46 -0700 | [diff] [blame] | 1 | # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | import logging |
| 6 | import unittest |
| 7 | import re |
| 8 | import csv |
| 9 | import common |
| 10 | import os |
| 11 | |
| 12 | from itertools import imap |
| 13 | from autotest_lib.server.cros import resource_monitor |
| 14 | from autotest_lib.server.hosts import abstract_ssh |
| 15 | from autotest_lib.server import utils |
| 16 | |
| 17 | class HostMock(abstract_ssh.AbstractSSHHost): |
| 18 | """Mocks a host object.""" |
| 19 | |
| 20 | TOP_PID = '1234' |
| 21 | |
| 22 | def _initialize(self, test_env): |
| 23 | self.top_is_running = False |
| 24 | |
| 25 | # Keep track of whether the top raw output file exists on the system, |
| 26 | # and if it does, where it is. |
| 27 | self.top_output_file_path = None |
| 28 | |
| 29 | # Keep track of whether the raw top output file is currently being |
| 30 | # written to by top. |
| 31 | self.top_output_file_is_open = False |
| 32 | self.test_env = test_env |
| 33 | |
| 34 | |
| 35 | def get_file(self, src, dest): |
| 36 | pass |
| 37 | |
| 38 | |
| 39 | def called_unsupported_command(self, command): |
| 40 | """Raises assertion error when called. |
| 41 | |
| 42 | @param command string the unsupported command called. |
| 43 | |
| 44 | """ |
| 45 | raise AssertionError( |
| 46 | "ResourceMonitor called unsupported command %s" % command) |
| 47 | |
| 48 | |
| 49 | def _process_top(self, cmd_args, cmd_line): |
| 50 | """Process top command. |
| 51 | |
| 52 | @param cmd_args string_list of command line args. |
| 53 | @param cmd_line string the command to run. |
| 54 | |
| 55 | """ |
| 56 | self.test_env.assertFalse(self.top_is_running, |
| 57 | msg="Top must not already be running.") |
| 58 | self.test_env.assertFalse(self.top_output_file_is_open, |
| 59 | msg="The top output file should not be being written " |
| 60 | "to before top is started") |
| 61 | self.test_env.assertIsNone(self.top_output_file_path, |
| 62 | msg="The top output file should not exist" |
| 63 | "before top is started") |
| 64 | try: |
| 65 | self.redirect_index = cmd_args.index(">") |
| 66 | self.top_output_file_path = cmd_args[self.redirect_index + 1] |
| 67 | except ValueError, IndexError: |
| 68 | self.called_unsupported_command(cmd_line) |
| 69 | |
| 70 | self.top_is_running = True |
| 71 | self.top_output_file_is_open = True |
| 72 | |
| 73 | return HostMock.TOP_PID |
| 74 | |
| 75 | |
| 76 | def _process_kill(self, cmd_args, cmd_line): |
| 77 | """Process kill command. |
| 78 | |
| 79 | @param cmd_args string_list of command line args. |
| 80 | @param cmd_line string the command to run. |
| 81 | |
| 82 | """ |
| 83 | try: |
| 84 | if cmd_args[1].startswith('-'): |
| 85 | pid_to_kill = cmd_args[2] |
| 86 | else: |
| 87 | pid_to_kill = cmd_args[1] |
| 88 | except IndexError: |
| 89 | self.called_unsupported_command(cmd_line) |
| 90 | |
| 91 | self.test_env.assertEqual(pid_to_kill, HostMock.TOP_PID, |
| 92 | msg="Wrong pid (%r) killed . Top pid is %r." % (pid_to_kill, |
| 93 | HostMock.TOP_PID)) |
| 94 | self.test_env.assertTrue(self.top_is_running, |
| 95 | msg="Top must be running before we try to kill it") |
| 96 | |
| 97 | self.top_is_running = False |
| 98 | self.top_output_file_is_open = False |
| 99 | |
| 100 | |
| 101 | def _process_rm(self, cmd_args, cmd_line): |
| 102 | """Process rm command. |
| 103 | |
| 104 | @param cmd_args string list list of command line args. |
| 105 | @param cmd_line string the command to run. |
| 106 | |
| 107 | """ |
| 108 | try: |
| 109 | if cmd_args[1].startswith('-'): |
| 110 | file_to_rm = cmd_args[2] |
| 111 | else: |
| 112 | file_to_rm = cmd_args[1] |
| 113 | except IndexError: |
| 114 | self.called_unsupported_command(cmd_line) |
| 115 | |
| 116 | self.test_env.assertEqual(file_to_rm, self.top_output_file_path, |
| 117 | msg="Tried to remove file that is not the top output file.") |
| 118 | self.test_env.assertFalse(self.top_output_file_is_open, |
| 119 | msg="Tried to remove top output file while top is still " |
| 120 | "writing to it.") |
| 121 | self.test_env.assertFalse(self.top_is_running, |
| 122 | msg="Top was still running when we tried to remove" |
| 123 | "the top output file.") |
| 124 | self.test_env.assertIsNotNone(self.top_output_file_path) |
| 125 | |
| 126 | self.top_output_file_path = None |
| 127 | |
| 128 | |
| 129 | def _run_single_cmd(self, cmd_line, *args, **kwargs): |
| 130 | """Run a single command on host. |
| 131 | |
| 132 | @param cmd_line command to run on the host. |
| 133 | |
| 134 | """ |
| 135 | # Make the input a little nicer. |
| 136 | cmd_line = cmd_line.strip() |
| 137 | cmd_line = re.sub(">", " > ", cmd_line) |
| 138 | |
| 139 | cmd_args = re.split("\s+", cmd_line) |
| 140 | self.test_env.assertTrue(len(cmd_args) >= 1) |
| 141 | command = cmd_args[0] |
| 142 | if (command == "top"): |
| 143 | return self._process_top(cmd_args, cmd_line) |
| 144 | elif (command == "kill"): |
| 145 | return self._process_kill(cmd_args, cmd_line) |
| 146 | elif(command == "rm"): |
| 147 | return self._process_rm(cmd_args, cmd_line) |
| 148 | else: |
| 149 | logging.warning("Called unemulated command %r", cmd_line) |
| 150 | return None |
| 151 | |
| 152 | |
| 153 | def run(self, cmd_line, *args, **kwargs): |
| 154 | """Run command(s) on host. |
| 155 | |
| 156 | @param cmd_line command to run on the host. |
| 157 | @return CmdResult object. |
| 158 | |
| 159 | """ |
| 160 | cmds = re.split("&&", cmd_line) |
| 161 | for cmd in cmds: |
| 162 | self._run_single_cmd(cmd) |
| 163 | return utils.CmdResult(exit_status=0) |
| 164 | |
| 165 | |
| 166 | def run_background(self, cmd_line, *args, **kwargs): |
| 167 | """Run command in background on host. |
| 168 | |
| 169 | @param cmd_line command to run on the host. |
| 170 | |
| 171 | """ |
| 172 | return self._run_single_cmd(cmd_line, args, kwargs) |
| 173 | |
| 174 | |
| 175 | def is_monitoring(self): |
| 176 | """Return true iff host is currently running top and writing output |
| 177 | to a file. |
| 178 | """ |
| 179 | return self.top_is_running and self.top_output_file_is_open and ( |
| 180 | self.top_output_file_path is not None) |
| 181 | |
| 182 | |
| 183 | def monitoring_stopped(self): |
| 184 | """Return true iff host is not running top and all top output files are |
| 185 | closed. |
| 186 | """ |
| 187 | return not self.is_monitoring() |
| 188 | |
| 189 | |
| 190 | class ResourceMonitorTest(unittest.TestCase): |
| 191 | """Tests the non-trivial functionality of ResourceMonitor.""" |
| 192 | |
| 193 | def setUp(self): |
| 194 | self.topoutfile = '/tmp/resourcemonitorunittest-1234' |
| 195 | self.monitor_period = 1 |
| 196 | self.rm_conf = resource_monitor.ResourceMonitorConfig( |
| 197 | monitor_period=self.monitor_period, |
| 198 | rawresult_output_filename=self.topoutfile) |
| 199 | self.host = HostMock(self) |
| 200 | |
| 201 | |
| 202 | def test_normal_operation(self): |
| 203 | """Checks that normal (i.e. no exceptions, etc.) execution works.""" |
| 204 | self.assertFalse(self.host.is_monitoring()) |
| 205 | with resource_monitor.ResourceMonitor(self.host, self.rm_conf) as rm: |
| 206 | self.assertFalse(self.host.is_monitoring()) |
| 207 | for i in range(3): |
| 208 | rm.start() |
| 209 | self.assertTrue(self.host.is_monitoring()) |
| 210 | rm.stop() |
| 211 | self.assertTrue(self.host.monitoring_stopped()) |
| 212 | self.assertTrue(self.host.monitoring_stopped()) |
| 213 | |
| 214 | |
| 215 | def test_forgot_to_stop_monitor(self): |
| 216 | """Checks that resource monitor is cleaned up even if user forgets to |
| 217 | explicitly stop it. |
| 218 | """ |
| 219 | self.assertFalse(self.host.is_monitoring()) |
| 220 | with resource_monitor.ResourceMonitor(self.host, self.rm_conf) as rm: |
| 221 | self.assertFalse(self.host.is_monitoring()) |
| 222 | rm.start() |
| 223 | self.assertTrue(self.host.is_monitoring()) |
| 224 | self.assertTrue(self.host.monitoring_stopped()) |
| 225 | |
| 226 | |
| 227 | def test_unexpected_interruption_while_monitoring(self): |
| 228 | """Checks that monitor is cleaned up upon unexpected interrupt.""" |
| 229 | self.assertFalse(self.host.is_monitoring()) |
| 230 | |
| 231 | with resource_monitor.ResourceMonitor(self.host, self.rm_conf) as rm: |
| 232 | self.assertFalse(self.host.is_monitoring()) |
| 233 | rm.start() |
| 234 | self.assertTrue(self.host.is_monitoring()) |
| 235 | raise KeyboardInterrupt |
| 236 | |
| 237 | self.assertTrue(self.host.monitoring_stopped()) |
| 238 | |
| 239 | |
| 240 | class ResourceMonitorResultTest(unittest.TestCase): |
| 241 | """Functional tests for ResourceMonitorParsedResult.""" |
| 242 | |
| 243 | def setUp(self): |
| 244 | self._res_dir = os.path.join( |
| 245 | os.path.dirname(os.path.realpath(__file__)), |
| 246 | 'res_resource_monitor') |
| 247 | |
| 248 | |
| 249 | def run_with_test_data(self, testdata_file, testans_file): |
| 250 | """Parses testdata_file with the parses, and checks that results |
| 251 | are the same as those in testans_file. |
| 252 | |
| 253 | @param testdata_file string filename containing top output to test. |
| 254 | @param testans_file string filename containing answers to the test. |
| 255 | |
| 256 | """ |
| 257 | parsed_results = resource_monitor.ResourceMonitorParsedResult( |
| 258 | testdata_file) |
| 259 | with open(testans_file, "rb") as testans: |
| 260 | csvreader = csv.reader(testans) |
| 261 | columns = csvreader.next() |
| 262 | self.assertEqual(list(columns), |
| 263 | resource_monitor.ResourceMonitorParsedResult._columns) |
| 264 | utils_over_time = [] |
| 265 | for util_val in imap( |
| 266 | resource_monitor. |
| 267 | ResourceMonitorParsedResult.UtilValues._make, |
| 268 | csvreader): |
| 269 | utils_over_time.append(util_val) |
| 270 | self.assertEqual(utils_over_time, parsed_results._utils_over_time) |
| 271 | |
| 272 | |
| 273 | def test_full_data(self): |
| 274 | """General test with many possible changes to input.""" |
| 275 | self.run_with_test_data( |
| 276 | os.path.join(self._res_dir, 'top_test_data.txt'), |
| 277 | os.path.join(self._res_dir, 'top_test_data_ans.csv')) |
| 278 | |
| 279 | |
| 280 | def test_whitespace_ridden(self): |
| 281 | """Tests resilience to arbitrary whitespace characters between fields""" |
| 282 | self.run_with_test_data( |
| 283 | os.path.join(self._res_dir, 'top_whitespace_ridden.txt'), |
| 284 | os.path.join(self._res_dir, 'top_whitespace_ridden_ans.csv')) |
| 285 | |
| 286 | |
| 287 | def test_field_order_changed(self): |
| 288 | """Tests resilience to changes in the order of fields |
| 289 | (for e.g, if the Mem free/used fields change orders in the input). |
| 290 | """ |
| 291 | self.run_with_test_data( |
| 292 | os.path.join(self._res_dir, 'top_field_order_changed.txt'), |
| 293 | os.path.join(self._res_dir, 'top_field_order_changed_ans.csv')) |
| 294 | |
| 295 | |
| 296 | if __name__ == '__main__': |
| 297 | unittest.main() |