306 lines
10 KiB
Python
306 lines
10 KiB
Python
# Lint as: python2, python3
|
|
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import logging
|
|
import unittest
|
|
import re
|
|
import csv
|
|
import common
|
|
import os
|
|
|
|
|
|
from autotest_lib.server.cros import resource_monitor
|
|
from autotest_lib.server.hosts import abstract_ssh
|
|
from autotest_lib.server import utils
|
|
import six
|
|
from six.moves import map
|
|
from six.moves import range
|
|
|
|
class HostMock(abstract_ssh.AbstractSSHHost):
|
|
"""Mocks a host object."""
|
|
|
|
TOP_PID = '1234'
|
|
|
|
def _initialize(self, test_env):
|
|
self.top_is_running = False
|
|
|
|
# Keep track of whether the top raw output file exists on the system,
|
|
# and if it does, where it is.
|
|
self.top_output_file_path = None
|
|
|
|
# Keep track of whether the raw top output file is currently being
|
|
# written to by top.
|
|
self.top_output_file_is_open = False
|
|
self.test_env = test_env
|
|
|
|
|
|
def get_file(self, src, dest):
|
|
pass
|
|
|
|
|
|
def called_unsupported_command(self, command):
|
|
"""Raises assertion error when called.
|
|
|
|
@param command string the unsupported command called.
|
|
|
|
"""
|
|
raise AssertionError(
|
|
"ResourceMonitor called unsupported command %s" % command)
|
|
|
|
|
|
def _process_top(self, cmd_args, cmd_line):
|
|
"""Process top command.
|
|
|
|
@param cmd_args string_list of command line args.
|
|
@param cmd_line string the command to run.
|
|
|
|
"""
|
|
self.test_env.assertFalse(self.top_is_running,
|
|
msg="Top must not already be running.")
|
|
self.test_env.assertFalse(self.top_output_file_is_open,
|
|
msg="The top output file should not be being written "
|
|
"to before top is started")
|
|
self.test_env.assertIsNone(self.top_output_file_path,
|
|
msg="The top output file should not exist"
|
|
"before top is started")
|
|
try:
|
|
self.redirect_index = cmd_args.index(">")
|
|
self.top_output_file_path = cmd_args[self.redirect_index + 1]
|
|
except (ValueError, IndexError):
|
|
self.called_unsupported_command(cmd_line)
|
|
|
|
self.top_is_running = True
|
|
self.top_output_file_is_open = True
|
|
|
|
return HostMock.TOP_PID
|
|
|
|
|
|
def _process_kill(self, cmd_args, cmd_line):
|
|
"""Process kill command.
|
|
|
|
@param cmd_args string_list of command line args.
|
|
@param cmd_line string the command to run.
|
|
|
|
"""
|
|
try:
|
|
if cmd_args[1].startswith('-'):
|
|
pid_to_kill = cmd_args[2]
|
|
else:
|
|
pid_to_kill = cmd_args[1]
|
|
except IndexError:
|
|
self.called_unsupported_command(cmd_line)
|
|
|
|
self.test_env.assertEqual(pid_to_kill, HostMock.TOP_PID,
|
|
msg="Wrong pid (%r) killed . Top pid is %r." % (pid_to_kill,
|
|
HostMock.TOP_PID))
|
|
self.test_env.assertTrue(self.top_is_running,
|
|
msg="Top must be running before we try to kill it")
|
|
|
|
self.top_is_running = False
|
|
self.top_output_file_is_open = False
|
|
|
|
|
|
def _process_rm(self, cmd_args, cmd_line):
|
|
"""Process rm command.
|
|
|
|
@param cmd_args string list list of command line args.
|
|
@param cmd_line string the command to run.
|
|
|
|
"""
|
|
try:
|
|
if cmd_args[1].startswith('-'):
|
|
file_to_rm = cmd_args[2]
|
|
else:
|
|
file_to_rm = cmd_args[1]
|
|
except IndexError:
|
|
self.called_unsupported_command(cmd_line)
|
|
|
|
self.test_env.assertEqual(file_to_rm, self.top_output_file_path,
|
|
msg="Tried to remove file that is not the top output file.")
|
|
self.test_env.assertFalse(self.top_output_file_is_open,
|
|
msg="Tried to remove top output file while top is still "
|
|
"writing to it.")
|
|
self.test_env.assertFalse(self.top_is_running,
|
|
msg="Top was still running when we tried to remove"
|
|
"the top output file.")
|
|
self.test_env.assertIsNotNone(self.top_output_file_path)
|
|
|
|
self.top_output_file_path = None
|
|
|
|
|
|
def _run_single_cmd(self, cmd_line, *args, **kwargs):
|
|
"""Run a single command on host.
|
|
|
|
@param cmd_line command to run on the host.
|
|
|
|
"""
|
|
# Make the input a little nicer.
|
|
cmd_line = cmd_line.strip()
|
|
cmd_line = re.sub(">", " > ", cmd_line)
|
|
|
|
cmd_args = re.split("\s+", cmd_line)
|
|
self.test_env.assertTrue(len(cmd_args) >= 1)
|
|
command = cmd_args[0]
|
|
if (command == "top"):
|
|
return self._process_top(cmd_args, cmd_line)
|
|
elif (command == "kill"):
|
|
return self._process_kill(cmd_args, cmd_line)
|
|
elif(command == "rm"):
|
|
return self._process_rm(cmd_args, cmd_line)
|
|
else:
|
|
logging.warning("Called unemulated command %r", cmd_line)
|
|
return None
|
|
|
|
|
|
def run(self, cmd_line, *args, **kwargs):
|
|
"""Run command(s) on host.
|
|
|
|
@param cmd_line command to run on the host.
|
|
@return CmdResult object.
|
|
|
|
"""
|
|
cmds = re.split("&&", cmd_line)
|
|
for cmd in cmds:
|
|
self._run_single_cmd(cmd)
|
|
return utils.CmdResult(exit_status=0)
|
|
|
|
|
|
def run_background(self, cmd_line, *args, **kwargs):
|
|
"""Run command in background on host.
|
|
|
|
@param cmd_line command to run on the host.
|
|
|
|
"""
|
|
return self._run_single_cmd(cmd_line, args, kwargs)
|
|
|
|
|
|
def is_monitoring(self):
|
|
"""Return true iff host is currently running top and writing output
|
|
to a file.
|
|
"""
|
|
return self.top_is_running and self.top_output_file_is_open and (
|
|
self.top_output_file_path is not None)
|
|
|
|
|
|
def monitoring_stopped(self):
|
|
"""Return true iff host is not running top and all top output files are
|
|
closed.
|
|
"""
|
|
return not self.is_monitoring()
|
|
|
|
|
|
class ResourceMonitorTest(unittest.TestCase):
|
|
"""Tests the non-trivial functionality of ResourceMonitor."""
|
|
|
|
def setUp(self):
|
|
self.topoutfile = '/tmp/resourcemonitorunittest-1234'
|
|
self.monitor_period = 1
|
|
self.rm_conf = resource_monitor.ResourceMonitorConfig(
|
|
monitor_period=self.monitor_period,
|
|
rawresult_output_filename=self.topoutfile)
|
|
self.host = HostMock(self)
|
|
|
|
|
|
def test_normal_operation(self):
|
|
"""Checks that normal (i.e. no exceptions, etc.) execution works."""
|
|
self.assertFalse(self.host.is_monitoring())
|
|
with resource_monitor.ResourceMonitor(self.host, self.rm_conf) as rm:
|
|
self.assertFalse(self.host.is_monitoring())
|
|
for i in range(3):
|
|
rm.start()
|
|
self.assertTrue(self.host.is_monitoring())
|
|
rm.stop()
|
|
self.assertTrue(self.host.monitoring_stopped())
|
|
self.assertTrue(self.host.monitoring_stopped())
|
|
|
|
|
|
def test_forgot_to_stop_monitor(self):
|
|
"""Checks that resource monitor is cleaned up even if user forgets to
|
|
explicitly stop it.
|
|
"""
|
|
self.assertFalse(self.host.is_monitoring())
|
|
with resource_monitor.ResourceMonitor(self.host, self.rm_conf) as rm:
|
|
self.assertFalse(self.host.is_monitoring())
|
|
rm.start()
|
|
self.assertTrue(self.host.is_monitoring())
|
|
self.assertTrue(self.host.monitoring_stopped())
|
|
|
|
|
|
def test_unexpected_interruption_while_monitoring(self):
|
|
"""Checks that monitor is cleaned up upon unexpected interrupt."""
|
|
self.assertFalse(self.host.is_monitoring())
|
|
|
|
with resource_monitor.ResourceMonitor(self.host, self.rm_conf) as rm:
|
|
self.assertFalse(self.host.is_monitoring())
|
|
rm.start()
|
|
self.assertTrue(self.host.is_monitoring())
|
|
raise KeyboardInterrupt
|
|
|
|
self.assertTrue(self.host.monitoring_stopped())
|
|
|
|
|
|
class ResourceMonitorResultTest(unittest.TestCase):
|
|
"""Functional tests for ResourceMonitorParsedResult."""
|
|
|
|
def setUp(self):
|
|
self._res_dir = os.path.join(
|
|
os.path.dirname(os.path.realpath(__file__)),
|
|
'res_resource_monitor')
|
|
|
|
|
|
def run_with_test_data(self, testdata_file, testans_file):
|
|
"""Parses testdata_file with the parses, and checks that results
|
|
are the same as those in testans_file.
|
|
|
|
@param testdata_file string filename containing top output to test.
|
|
@param testans_file string filename containing answers to the test.
|
|
|
|
"""
|
|
parsed_results = resource_monitor.ResourceMonitorParsedResult(
|
|
testdata_file)
|
|
with open(testans_file, "rb") as testans:
|
|
csvreader = csv.reader(testans)
|
|
columns = next(csvreader)
|
|
self.assertEqual(list(columns),
|
|
resource_monitor.ResourceMonitorParsedResult._columns)
|
|
utils_over_time = []
|
|
for util_val in map(
|
|
resource_monitor.
|
|
ResourceMonitorParsedResult.UtilValues._make,
|
|
csvreader):
|
|
utils_over_time.append(util_val)
|
|
self.assertEqual(utils_over_time, parsed_results._utils_over_time)
|
|
|
|
|
|
def test_full_data(self):
|
|
"""General test with many possible changes to input."""
|
|
self.run_with_test_data(
|
|
os.path.join(self._res_dir, 'top_test_data.txt'),
|
|
os.path.join(self._res_dir, 'top_test_data_ans.csv'))
|
|
|
|
|
|
def test_whitespace_ridden(self):
|
|
"""Tests resilience to arbitrary whitespace characters between fields"""
|
|
self.run_with_test_data(
|
|
os.path.join(self._res_dir, 'top_whitespace_ridden.txt'),
|
|
os.path.join(self._res_dir, 'top_whitespace_ridden_ans.csv'))
|
|
|
|
|
|
def test_field_order_changed(self):
|
|
"""Tests resilience to changes in the order of fields
|
|
(for e.g, if the Mem free/used fields change orders in the input).
|
|
"""
|
|
self.run_with_test_data(
|
|
os.path.join(self._res_dir, 'top_field_order_changed.txt'),
|
|
os.path.join(self._res_dir, 'top_field_order_changed_ans.csv'))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|