Integration test to test the basic functionality of dev-install and gmerge.
This module contains a test that runs some sanity integration tests against
a VM. First it starts a VM test image and turns it into a base image by wiping
all of the stateful partition. Once done, runs dev_install to restore the
stateful partition and then runs gmerge.
Right now I have the gmerge test not run as it's having some funky portage
issues (I could switch it to qemacs and it'll work -- see TODO).
BUG=chromium-os:12388 chromium-os:37901
TEST=Ran it --- many many times.
Change-Id: I32a230514d07d76b3d6cd9029181a374826c4104
Reviewed-on: https://gerrit.chromium.org/gerrit/44762
Commit-Queue: Chris Sosa <sosa@chromium.org>
Reviewed-by: Chris Sosa <sosa@chromium.org>
Tested-by: Chris Sosa <sosa@chromium.org>
diff --git a/devmode-test/README b/devmode-test/README
new file mode 100644
index 0000000..4cb44f3
--- /dev/null
+++ b/devmode-test/README
@@ -0,0 +1,8 @@
+# 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.
+
+Folder containing tests for dev-mode related programs like dev-install.
+
+These tests run from a VM and do not use autotest as they muck with the entire
+stateful partition which is necessary for autotest tests.
diff --git a/devmode-test/constants.py b/devmode-test/constants.py
new file mode 120000
index 0000000..8d73346
--- /dev/null
+++ b/devmode-test/constants.py
@@ -0,0 +1 @@
+../lib/constants.py
\ No newline at end of file
diff --git a/devmode-test/devinstall_test.py b/devmode-test/devinstall_test.py
new file mode 100755
index 0000000..b47f4cc
--- /dev/null
+++ b/devmode-test/devinstall_test.py
@@ -0,0 +1,217 @@
+#!/usr/bin/python
+#
+# 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.
+
+"""Integration test to test the basic functionality of dev-install and gmerge.
+
+This module contains a test that runs some sanity integration tests against
+a VM. First it starts a VM test image and turns it into a base image by wiping
+all of the stateful partition. Once done, runs dev_install to restore the
+stateful partition and then runs gmerge.
+"""
+
+import logging
+import optparse
+import os
+import shutil
+import socket
+import sys
+import tempfile
+
+import constants
+sys.path.append(constants.SOURCE_ROOT)
+sys.path.append(constants.CROS_PLATFORM_ROOT)
+
+from chromite.lib import cros_build_lib
+from crostestutils.lib import dev_server_wrapper
+from crostestutils.lib import mount_helper
+from crostestutils.lib import test_helper
+
+
+_LOCALHOST = 'localhost'
+_PRIVATE_KEY = os.path.join(constants.CROSUTILS_DIR, 'mod_for_test_scripts',
+ 'ssh_keys', 'testing_rsa')
+
+class TestError(Exception):
+ """Raised on any error during testing. It being raised is a test failure."""
+
+
+class DevModeTest(object):
+ """Wrapper for dev mode tests."""
+ def __init__(self, image_path, board, binhost):
+ """
+ Args:
+ image_path: Filesystem path to the image to test.
+ board: Board of the image under test.
+ binhost: Binhost override. Binhost as defined here is where dev-install
+ or gmerge go to search for binary packages. By default this will
+ be set to the devserver url of the host running this script.
+ If no override i.e. the default is ok, set to None.
+ """
+ self.image_path = image_path
+ self.board = board
+ self.binhost = binhost
+
+ self.tmpdir = tempfile.mkdtemp('DevModeTest')
+ self.tmpsshkey = os.path.join(self.tmpdir, 'testing_rsa')
+ self.tmpkvmpid = os.path.join(self.tmpdir, 'kvm_pid')
+
+ self.working_image_path = None
+ self.devserver = None
+ self.port = None
+
+ def Cleanup(self):
+ """Clean up any state at the end of the test."""
+ try:
+ if self.working_image_path:
+ os.remove(self.working_image_path)
+
+ if self.devserver:
+ self.devserver.Stop()
+
+ self.devserver = None
+
+ cmd = ['%s/bin/cros_stop_vm' % constants.CROSUTILS_DIR,
+ '--kvm_pid', self.tmpkvmpid]
+ cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
+
+ if self.tmpdir:
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
+
+ self.tmpdir = None
+ except Exception:
+ logging.warning('Received error during cleanup', exc_info=True)
+
+ def _SetupSSH(self):
+ """Sets up the necessary items for running ssh."""
+ self.port = self._FindUnusedPort()
+ shutil.copyfile(_PRIVATE_KEY, self.tmpsshkey)
+ os.chmod(self.tmpsshkey, 0400)
+
+ def _RunSSHCommand(self, command, **kwargs):
+ """Runs ssh command (a list) using RunCommand (kwargs for RunCommand)."""
+ assert isinstance(command, list)
+ assert self.port and self.tmpsshkey, 'Tried to run ssh before ssh was setup'
+ kwargs.update(dict(debug_level=logging.DEBUG))
+ return cros_build_lib.RunCommand(
+ ['ssh', '-n', '-p', str(self.port), '-i', self.tmpsshkey,
+ 'root@%s' % _LOCALHOST, '--'] + command, **kwargs)
+
+ def _WipeStatefulPartition(self):
+ """Deletes everything from the working image path's stateful partition."""
+ r_mount_point = os.path.join(self.tmpdir, 'm')
+ s_mount_point = os.path.join(self.tmpdir, 's')
+ mount_helper.MountImage(self.working_image_path,
+ r_mount_point, s_mount_point, read_only=False,
+ safe=True)
+ # Run in shell mode to interpret '*' as a glob.
+ cros_build_lib.SudoRunCommand('rm -rf %s/*' % s_mount_point, shell=True,
+ debug_level=logging.DEBUG)
+ mount_helper.UnmountImage(r_mount_point, s_mount_point)
+
+ def _FindUnusedPort(self):
+ """Returns a currently unused port."""
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.bind((_LOCALHOST, 0))
+ port = s.getsockname()[1]
+ s.close()
+ return port
+
+ def PrepareTest(self):
+ """Pre-test modification to the image and env to setup test."""
+ logging.info('Setting up the image %s for vm testing.',
+ self.image_path)
+ self._SetupSSH()
+ vm_path = test_helper.CreateVMImage(self.image_path, self.board,
+ full=False)
+
+ logging.info('Making copy of the vm image %s to manipulate.', vm_path)
+ self.working_image_path = vm_path + '.' + str(self.port)
+ shutil.copyfile(vm_path, self.working_image_path)
+ logging.debug('Copy of vm image stored at %s.', self.working_image_path)
+
+ logging.info('Wiping the stateful partition to prepare test.')
+ self._WipeStatefulPartition()
+
+ logging.info('Starting the vm on port %d.', self.port)
+ cmd = ['%s/bin/cros_start_vm' % constants.CROSUTILS_DIR,
+ '--ssh_port', str(self.port),
+ '--image_path', self.working_image_path,
+ '--no_graphics',
+ '--kvm_pid', self.tmpkvmpid]
+ cros_build_lib.RunCommand(cmd, debug_level=logging.DEBUG)
+
+ if not self.binhost:
+ logging.info('Starting the devserver.')
+ self.devserver = dev_server_wrapper.DevServerWrapper(self.tmpdir)
+ self.devserver.start()
+ self.devserver.WaitUntilStarted()
+ self.binhost = dev_server_wrapper.DevServerWrapper.GetDevServerURL(
+ None, 'static/pkgroot/%s/packages' % self.board)
+
+ logging.info('Using binhost %s', self.binhost)
+
+ def TestDevInstall(self):
+ """Tests that we can run dev-install and have python work afterwards."""
+ try:
+ logging.info('Running dev install in the vm.')
+ self._RunSSHCommand(
+ ['bash', '-l', '-c',
+ '"/usr/bin/dev_install --yes --binhost %s"' % self.binhost])
+
+ logging.info('Verifying that python works on the image.')
+ self._RunSSHCommand(
+ ['sudo', '-u', 'chronos', '--',
+ 'python', '-c', '"print \'hello world\'"'])
+ except cros_build_lib.RunCommandError as e:
+ self.devserver.PrintLog()
+ logging.error('dev-install test failed. See devserver log above for more '
+ 'details.')
+ raise TestError('dev-install test failed with: %s' % str(e))
+
+ # TODO(sosa): Currently not run as emerge is not respecting package.provided
+ # after dev_install in a VM. Cannot repro on a device.
+ def TestGmerge(self):
+ """Evaluates whether the test passed or failed."""
+ logging.info('Verifying that python works on the image after dev install.')
+ try:
+ self._RunSSHCommand(['gmerge', 'gmerge', '--accept_stable', '--usepkg'])
+ except cros_build_lib.RunCommandError as e:
+ logging.error('gmerge test failed. See log for details')
+ raise TestError('gmerge test failed with: %s' % str(e))
+
+
+def main():
+ usage = ('%s <board> <path_to_[test|vm]_image>. '
+ 'See --help for more options' % os.path.basename(sys.argv[0]))
+ parser = optparse.OptionParser(usage)
+ parser.add_option('--binhost', metavar='URL',
+ help='binhost override. By default, starts up a devserver '
+ 'and uses it as the binhost.')
+ parser.add_option('-v', '--verbose', default=False, action='store_true',
+ help='Print out added debugging information')
+
+ (options, args) = parser.parse_args()
+
+ if len(args) != 2:
+ parser.print_usage()
+ parser.error('Need board and path to test image.')
+
+ board = args[0]
+ image_path = os.path.realpath(args[1])
+
+ test_helper.SetupCommonLoggingFormat(verbose=options.verbose)
+
+ test = DevModeTest(image_path, board, options.binhost)
+ try:
+ test.PrepareTest()
+ test.TestDevInstall()
+ logging.info('All tests passed.')
+ finally:
+ test.Cleanup()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/lib/dev_server_wrapper.py b/lib/dev_server_wrapper.py
index dd97977..684a5f3 100644
--- a/lib/dev_server_wrapper.py
+++ b/lib/dev_server_wrapper.py
@@ -76,7 +76,7 @@
"""Print devserver output."""
print '--- Start output from %s ---' % self._log_filename
# Open in update mode in case the child process hasn't opened the file yet.
- with open(self._log_filename, 'w+') as log:
+ with open(self._log_filename) as log:
sys.stdout.writelines(log)
print '--- End output from %s ---' % self._log_filename
@@ -105,9 +105,10 @@
def GetDevServerURL(cls, port, sub_dir):
"""Returns the dev server url for a given port and sub directory."""
if not port: port = 8080
- url = 'http://%(ip)s:%(port)s/%(dir)s' % {'ip': GetIPAddress(),
- 'port': str(port),
- 'dir': sub_dir}
+ url = 'http://%(ip)s:%(port)s' % {'ip': GetIPAddress(), 'port': str(port)}
+ if sub_dir:
+ url += '/' + sub_dir
+
return url
@classmethod
diff --git a/lib/mount_helper.py b/lib/mount_helper.py
new file mode 100644
index 0000000..16e9973
--- /dev/null
+++ b/lib/mount_helper.py
@@ -0,0 +1,31 @@
+# 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.
+
+# Module containing helper methods for mounting and unmounting an image.
+
+import os
+
+import constants
+from chromite.lib import cros_build_lib
+
+def MountImage(image_path, root_dir, stateful_dir, read_only, safe=False):
+ """Mounts a Chromium OS image onto mount dir points."""
+ from_dir, image = os.path.split(image_path)
+ cmd = ['./mount_gpt_image.sh',
+ '--from=%s' % from_dir,
+ '--image=%s' % image,
+ '--rootfs_mountpt=%s' % root_dir,
+ '--stateful_mountpt=%s' % stateful_dir]
+ if read_only: cmd.append('--read_only')
+ if safe: cmd.append('--safe')
+ cros_build_lib.RunCommandCaptureOutput(
+ cmd, print_cmd=False, cwd=constants.CROSUTILS_DIR)
+
+
+def UnmountImage(root_dir, stateful_dir):
+ """Unmounts a Chromium OS image specified by mount dir points."""
+ cmd = ['./mount_gpt_image.sh', '--unmount', '--rootfs_mountpt=%s' % root_dir,
+ '--stateful_mountpt=%s' % stateful_dir]
+ cros_build_lib.RunCommandCaptureOutput(
+ cmd, print_cmd=False, cwd=constants.CROSUTILS_DIR)
diff --git a/lib/public_key_manager.py b/lib/public_key_manager.py
index 723b2db..c3aca6f 100644
--- a/lib/public_key_manager.py
+++ b/lib/public_key_manager.py
@@ -11,27 +11,7 @@
from chromite.lib import cros_build_lib
from chromite.lib import git
from chromite.lib import osutils
-
-
-def MountImage(image_path, root_dir, stateful_dir, read_only):
- """Mounts a Chromium OS image onto mount dir points."""
- from_dir, image = os.path.split(image_path)
- cmd = ['./mount_gpt_image.sh',
- '--from=%s' % from_dir,
- '--image=%s' % image,
- '--rootfs_mountpt=%s' % root_dir,
- '--stateful_mountpt=%s' % stateful_dir]
- if read_only: cmd.append('--read_only')
- cros_build_lib.RunCommandCaptureOutput(
- cmd, print_cmd=False, cwd=constants.CROSUTILS_DIR)
-
-
-def UnmountImage(root_dir, stateful_dir):
- """Unmounts a Chromium OS image specified by mount dir points."""
- cmd = ['./mount_gpt_image.sh', '--unmount', '--rootfs_mountpt=%s' % root_dir,
- '--stateful_mountpt=%s' % stateful_dir]
- cros_build_lib.RunCommandCaptureOutput(
- cmd, print_cmd=False, cwd=constants.CROSUTILS_DIR)
+from crostestutils.lib import mount_helper
class PublicKeyManager(object):
@@ -47,7 +27,7 @@
# Gather some extra information about the image.
try:
- MountImage(image_path, self._rootfs_dir, self._stateful_dir,
+ mount_helper.MountImage(image_path, self._rootfs_dir, self._stateful_dir,
read_only=True)
self._full_target_key_path = os.path.join(
self._rootfs_dir, PublicKeyManager.TARGET_KEY_PATH)
@@ -59,7 +39,7 @@
if not res.output: self._is_key_new = False
finally:
- UnmountImage(self._rootfs_dir, self._stateful_dir)
+ mount_helper.UnmountImage(self._rootfs_dir, self._stateful_dir)
def __del__(self):
"""Remove our temporary directories we created in init."""
@@ -75,14 +55,14 @@
cros_build_lib.Info('Copying %s into %s', self.key_path, self.image_path)
try:
- MountImage(self.image_path, self._rootfs_dir, self._stateful_dir,
- read_only=False)
+ mount_helper.MountImage(self.image_path, self._rootfs_dir,
+ self._stateful_dir, read_only=False)
dir_path = os.path.dirname(self._full_target_key_path)
osutils.SafeMakedirs(dir_path, sudo=True)
cmd = ['cp', '--force', '-p', self.key_path, self._full_target_key_path]
cros_build_lib.SudoRunCommand(cmd)
finally:
- UnmountImage(self._rootfs_dir, self._stateful_dir)
+ mount_helper.UnmountImage(self._rootfs_dir, self._stateful_dir)
self._MakeImageBootable()
def _MakeImageBootable(self):
diff --git a/lib/test_helper.py b/lib/test_helper.py
index 96bff53..157a49a 100644
--- a/lib/test_helper.py
+++ b/lib/test_helper.py
@@ -39,7 +39,7 @@
return max(1, min(cpu_count, mem_count, loop_count))
-def CreateVMImage(image, board):
+def CreateVMImage(image, board=None, full=True):
"""Returns the path of the image built to run in a VM.
VM returned is a test image that can run full update testing on it. This
@@ -47,26 +47,30 @@
Args:
image: Path to the image.
- board: Board that the image was built with.
+ board: Board that the image was built with. If None, attempts to use the
+ configured default board.
+ full: If the vm image doesn't exist, create a "full" one which supports AU.
"""
vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname(image)
if not os.path.exists(vm_image_path):
logging.info('Creating %s', vm_image_path)
- cros_build_lib.RunCommand(
- ['./image_to_vm.sh',
- '--full',
- '--from=%s' % git.ReinterpretPathForChroot(os.path.dirname(image)),
- '--board=%s' % board,
- '--test_image'
- ], enter_chroot=True, cwd=constants.SOURCE_ROOT)
+ cmd = ['./image_to_vm.sh',
+ '--from=%s' % git.ReinterpretPathForChroot(os.path.dirname(image)),
+ '--test_image']
+ if full:
+ cmd.extend(['--full'])
+ if board:
+ cmd.extend(['--board', board])
+
+ cros_build_lib.RunCommand(cmd, enter_chroot=True, cwd=constants.SOURCE_ROOT)
assert os.path.exists(vm_image_path), 'Failed to create the VM image.'
return vm_image_path
-def SetupCommonLoggingFormat():
+def SetupCommonLoggingFormat(verbose=True):
"""Sets up common logging format for the logging module."""
logging_format = '%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s'
date_format = '%Y/%m/%d %H:%M:%S'
- logging.basicConfig(level=logging.DEBUG, format=logging_format,
- datefmt=date_format)
+ logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO,
+ format=logging_format, datefmt=date_format)