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
5import logging
6import unittest
7import re
8import csv
9import common
10import os
11
12from itertools import imap
13from autotest_lib.server.cros import resource_monitor
14from autotest_lib.server.hosts import abstract_ssh
15from autotest_lib.server import utils
16
17class 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
190class 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
240class 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
296if __name__ == '__main__':
297    unittest.main()
298