blob: 71f3667e2975a3fab43e03840dddb2d40e5c9a28 [file] [log] [blame]
# Copyright 2015 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module for integration VM tests for CLI commands.
This module contains the basic functionalities for setting up a VM and testing
the CLI commands.
"""
import logging
from chromite.cli import deploy
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import remote_access
from chromite.lib import vm
from chromite.utils import outcap
class Error(Exception):
"""Base exception for CLI command VM tests."""
class SetupError(Error):
"""Raised when error occurs during test environment setup."""
class TestError(Error):
"""Raised when a command test has failed."""
class CommandError(Error):
"""Raised when error occurs during a command test."""
def _PrintCommandLog(command, content):
"""Print out the log |content| for |command|."""
if content:
logging.info(
"\n----------- Start of %s log -----------\n%s\n"
"----------- End of %s log -----------",
command,
content.rstrip(),
command,
)
def test_command_decorator(command_name):
"""Decorator that runs the command test function."""
def Decorator(test_function):
"""Inner decorator that actually wraps the function."""
def Wrapper(command_test):
"""Wrapper for the test function."""
command = cros_build_lib.CmdToStr(
command_test.BuildCommand(command_name)
)
logging.info("Running test for %s.", command)
try:
test_function(command_test)
logging.info("Test for %s passed.", command)
except CommandError as e:
_PrintCommandLog(command, str(e))
raise TestError("Test for %s failed." % command)
return Wrapper
return Decorator
class CommandVMTest(object):
"""Base class for CLI command VM tests.
This class provides the abstract interface for testing CLI commands on a VM.
The sub-class must define the BuildCommand method in order to be usable. And
the test functions must use the test_command_decorator decorator.
"""
def __init__(self, board, image_path):
"""Initializes CommandVMTest.
Args:
board: Board for the VM to run tests.
image_path: Path to the image for the VM to run tests.
"""
self.board = board
self.image_path = image_path
self.port = None
self.device_addr = None
def BuildCommand(self, command, device=None, pos_args=None, opt_args=None):
"""Builds a CLI command.
Args:
command: The sub-command to build on (e.g. 'flash', 'deploy').
device: The device's address for the command.
pos_args: A list of positional arguments for the command.
opt_args: A list of optional arguments for the command.
"""
raise NotImplementedError()
def SetUp(self):
"""Creates and starts the VM instance for testing."""
self.port = remote_access.GetUnusedPort()
self.device_addr = "ssh://%s:%d" % (remote_access.LOCALHOST, self.port)
vm_path = vm.CreateVMImage(
image=self.image_path, board=self.board, updatable=True
)
vm_cmd = [
"./cros_vm",
"--ssh-port=%d" % self.port,
"--copy-on-write",
"--board=%s" % self.board,
"--image-path=%s" % vm_path,
"--start",
]
cros_build_lib.run(vm_cmd, cwd=constants.CHROMITE_BIN_DIR)
def TearDown(self):
"""Stops the VM instance after testing."""
if not self.port:
return
cros_build_lib.run(
["./cros_vm", "--stop", "--ssh-port=%d" % self.port],
cwd=constants.CHROMITE_BIN_DIR,
check=False,
)
@test_command_decorator("shell")
def TestShell(self):
"""Tests the shell command."""
# The path and content of a temporary file for testing shell command.
path = "/tmp/shell-test"
content = "shell command test file"
cmd = self.BuildCommand(
"shell", device=self.device_addr, opt_args=["--no-known-hosts"]
)
logging.info(
"Test to use shell command to write a file to the VM device."
)
write_cmd = cmd + ["--", 'echo "%s" > %s' % (content, path)]
result = cros_build_lib.run(write_cmd, capture_output=True, check=False)
if result.returncode:
logging.error("Failed to write the file to the VM device.")
raise CommandError(result.stderr)
logging.info(
"Test to use shell command to read a file on the VM device."
)
read_cmd = cmd + ["--", "cat %s" % path]
result = cros_build_lib.run(
read_cmd, capture_output=True, encoding="utf-8", check=False
)
if result.returncode or result.stdout.rstrip() != content:
logging.error("Failed to read the file on the VM device.")
raise CommandError(result.stderr)
logging.info(
"Test to use shell command to remove a file on the VM device."
)
remove_cmd = cmd + ["--", "rm %s" % path]
result = cros_build_lib.run(
remove_cmd, capture_output=True, check=False
)
if result.returncode:
logging.error("Failed to remove the file on the VM device.")
raise CommandError(result.stderr)
@test_command_decorator("debug")
def TestDebug(self):
"""Tests the debug command."""
logging.info("Test to start and debug a new process on the VM device.")
exe_path = "/bin/bash"
start_cmd = self.BuildCommand(
"debug", device=self.device_addr, opt_args=["--exe", exe_path]
)
result = cros_build_lib.run(
start_cmd, capture_output=True, check=False, input="\n"
)
if result.returncode:
logging.error(
"Failed to start and debug a new process on the VM device."
)
raise CommandError(result.stderr)
logging.info("Test to attach a running process on the VM device.")
with remote_access.ChromiumOSDeviceHandler(
remote_access.LOCALHOST, port=self.port
) as device:
exe = "update_engine"
pids = device.GetRunningPids(exe, full_path=False)
if not pids:
logging.error("Failed to find any running process to debug.")
raise CommandError()
pid = pids[0]
attach_cmd = self.BuildCommand(
"debug", device=self.device_addr, opt_args=["--pid", str(pid)]
)
result = cros_build_lib.run(
attach_cmd, capture_output=True, check=False, input="\n"
)
if result.returncode:
logging.error(
"Failed to attach a running process on the VM device."
)
raise CommandError(result.stderr)
@test_command_decorator("flash")
def TestFlash(self):
"""Tests the flash command."""
# We explicitly disable reboot after the update because VMs sometimes do
# not come back after reboot. The flash command does not need to verify
# the integrity of the updated image. We have AU tests for that.
cmd = self.BuildCommand(
"flash",
device=self.device_addr,
pos_args=["latest"],
opt_args=["--no-wipe", "--no-reboot"],
)
logging.info("Test to flash the VM device with the latest image.")
result = cros_build_lib.run(cmd, capture_output=True, check=False)
if result.returncode:
logging.error("Failed to flash the VM device.")
raise CommandError(result.stderr)
@test_command_decorator("deploy")
def TestDeploy(self):
"""Tests the deploy command."""
packages = ["dev-python/cherrypy", "app-portage/portage-utils"]
# Set the installation root to /usr/local so that the command does not
# attempt to remount rootfs (which leads to VM reboot).
cmd = self.BuildCommand(
"deploy",
device=self.device_addr,
pos_args=packages,
opt_args=["--log-level=info", "--root=/usr/local"],
)
logging.info("Test to uninstall packages on the VM device.")
with outcap.OutputCapturer() as output:
result = cros_build_lib.run(cmd + ["--unmerge"], check=False)
if result.returncode:
logging.error("Failed to uninstall packages on the VM device.")
raise CommandError(result.stderr)
captured_output = output.GetStdout() + output.GetStderr()
for event in deploy.BrilloDeployOperation.UNMERGE_EVENTS:
if event not in captured_output:
logging.error(
"Strings used by deploy.BrilloDeployOperation to update "
"the progress bar have been changed. Please update the "
"strings in UNMERGE_EVENTS"
)
raise CommandError()
logging.info("Test to install packages on the VM device.")
with outcap.OutputCapturer() as output:
result = cros_build_lib.run(cmd, check=False)
if result.returncode:
logging.error("Failed to install packages on the VM device.")
raise CommandError(result.stderr)
captured_output = output.GetStdout() + output.GetStderr()
for event in deploy.BrilloDeployOperation.MERGE_EVENTS:
if event not in captured_output:
logging.error(
"Strings used by deploy.BrilloDeployOperation to update "
"the progress bar have been changed. Please update the "
"strings in MERGE_EVENTS"
)
raise CommandError()
# Verify that the packages are installed.
with remote_access.ChromiumOSDeviceHandler(
remote_access.LOCALHOST, port=self.port
) as device:
try:
device.run(["python", "-c", '"import cherrypy"'])
device.run(["qmerge", "-h"])
except cros_build_lib.RunCommandError as e:
logging.error(
"Unable to verify packages installed on VM: %s", e
)
raise CommandError()
def RunTests(self):
"""Calls the test functions."""
self.TestShell()
# TestDebug broken (crbug.com/863122)
self.TestFlash()
self.TestDeploy()
def Run(self):
"""Runs the tests."""
try:
self.SetUp()
self.RunTests()
logging.info("All tests completed successfully.")
finally:
self.TearDown()