blob: b95a842917fb99ee3f4448b4fcb03076d395224c [file] [log] [blame]
# Copyright 2014 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.
"""Collection of tests to run on the rootfs of a built image."""
import itertools
import logging
import os
import unittest
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import perf_uploader
# File extension for file containing performance values.
PERF_EXTENSION = '.perf'
# Symlinks to mounted partitions.
ROOT_A = 'dir-ROOT-A'
STATEFUL = 'dir-STATE'
def IsPerfFile(file_name):
"""Return True if |file_name| may contain perf values."""
return file_name.endswith(PERF_EXTENSION)
class BoardAndDirectoryMixin(object):
"""A mixin to hold image test's specific info."""
_board = None
_result_dir = None
def SetBoard(self, board):
self._board = board
def SetResultDir(self, result_dir):
self._result_dir = result_dir
class ImageTestCase(unittest.TestCase, BoardAndDirectoryMixin):
"""Subclass unittest.TestCase to provide utility methods for image tests.
Tests should not directly inherit this class. They should instead inherit
from ForgivingImageTestCase, or NonForgivingImageTestCase.
Tests MUST use prefix "Test" (e.g.: TestLinkage, TestDiskSpace), not "test"
prefix, in order to be picked up by the test runner.
Tests are run outside chroot.
The current working directory is set up so that "ROOT_A", and "STATEFUL"
constants refer to the mounted partitions. The partitions are mounted
readonly.
current working directory
+ ROOT_A
+ /
+ bin
+ etc
+ usr
...
+ STATEFUL
+ var_overlay
...
"""
def IsForgiving(self):
"""Indicate if this test is forgiving.
The test runner will classify tests into two buckets, forgiving and non-
forgiving. Forgiving tests DO NOT affect the result of the test runner;
non-forgiving tests do. In either case, test runner will still output the
result of each individual test.
"""
raise NotImplementedError()
def _GeneratePerfFileName(self):
"""Return a perf file name for this test.
The file name is formatted as:
image_test.<test_class><PERF_EXTENSION>
e.g.:
image_test.DiskSpaceTest.perf
"""
test_name = 'image_test.%s' % self.__class__.__name__
file_name = '%s%s' % (test_name, PERF_EXTENSION)
file_name = os.path.join(self._result_dir, file_name)
return file_name
@staticmethod
def GetTestName(file_name):
"""Return the test name from a perf |file_name|.
Args:
file_name: A path to the perf file as generated by _GeneratePerfFileName.
Returns:
The qualified test name part of the file name.
"""
file_name = os.path.basename(file_name)
pos = file_name.rindex('.')
return file_name[:pos]
def OutputPerfValue(self, description, value, units,
higher_is_better=True, graph=None):
"""Record a perf value.
If graph name is not provided, the test method name will be used as the
graph name.
Args:
description: A string description of the value such as "partition-0". A
special description "ref" is taken as the reference.
value: A float value.
units: A string describing the unit of measurement such as "KB", "meter".
higher_is_better: A boolean indicating if higher value means better
performance.
graph: A string name of the graph this value will be plotted on. If not
provided, the graph name will take the test method name.
"""
if not self._result_dir:
logging.warning('Result directory is not set. Ignore OutputPerfValue.')
return
if graph is None:
graph = self._testMethodName
file_name = self._GeneratePerfFileName()
perf_uploader.OutputPerfValue(file_name, description, value, units,
higher_is_better, graph)
class ForgivingImageTestCase(ImageTestCase):
"""Concrete base class of forgiving tests."""
def IsForgiving(self):
return True
class NonForgivingImageTestCase(ImageTestCase):
"""Concrete base class of non forgiving tests."""
def IsForgiving(self):
return False
class ImageTestSuite(unittest.TestSuite, BoardAndDirectoryMixin):
"""Wrap around unittest.TestSuite to pass more info to the actual tests."""
def GetTests(self):
return self._tests
def run(self, result, debug=False):
for t in self._tests:
t.SetResultDir(self._result_dir)
t.SetBoard(self._board)
return super(ImageTestSuite, self).run(result)
class ImageTestRunner(unittest.TextTestRunner, BoardAndDirectoryMixin):
"""Wrap around unittest.TextTestRunner to pass more info down the chain."""
def run(self, test):
test.SetResultDir(self._result_dir)
test.SetBoard(self._board)
return super(ImageTestRunner, self).run(test)
#####################
# Here go the tests
#####################
class LocaltimeTest(NonForgivingImageTestCase):
"""Verify that /etc/localtime is a symlink to /var/lib/timezone/localtime.
This is an example of an image test. The image is already mounted. The
test can access rootfs via ROOT_A constant.
"""
def TestLocaltimeIsSymlink(self):
localtime_path = os.path.join(ROOT_A, 'etc', 'localtime')
self.assertTrue(os.path.islink(localtime_path))
def TestLocaltimeLinkIsCorrect(self):
localtime_path = os.path.join(ROOT_A, 'etc', 'localtime')
self.assertEqual('/var/lib/timezone/localtime',
os.readlink(localtime_path))
class BlacklistTest(NonForgivingImageTestCase):
"""Verify that rootfs does not contain blacklisted directories."""
def TestBlacklistedDirectories(self):
dirs = [os.path.join(ROOT_A, 'usr', 'share', 'locale')]
for d in dirs:
self.assertFalse(os.path.isdir(d), 'Directory %s is blacklisted.' % d)
class LinkageTest(NonForgivingImageTestCase):
"""Verify that all binaries and libraries have proper linkage."""
def setUp(self):
self._outside_chroot = os.getcwd()
try:
self._inside_chroot = cros_build_lib.ToChrootPath(self._outside_chroot)
except ValueError:
self._inside_chroot = self._outside_chroot
osutils.MountDir(
os.path.join(self._outside_chroot, STATEFUL, 'var_overlay'),
os.path.join(self._outside_chroot, ROOT_A, 'var'),
mount_opts=('bind', ),
)
def tearDown(self):
osutils.UmountDir(
os.path.join(self._outside_chroot, ROOT_A, 'var'),
cleanup=False,
)
def _IsPackageMerged(self, package_name):
cmd = [
'portageq-%s' % self._board,
'has_version',
os.path.join(self._inside_chroot, ROOT_A),
package_name
]
ret = cros_build_lib.RunCommand(cmd, error_code_ok=True,
enter_chroot=True)
return ret.returncode == 0
def TestLinkage(self):
"""Find main executable binaries and check their linkage."""
binaries = [
'boot/vmlinuz',
'bin/sed',
]
if self._IsPackageMerged('chromeos-base/chromeos-login'):
binaries.append('sbin/session_manager')
if self._IsPackageMerged('x11-base/xorg-server'):
binaries.append('usr/bin/Xorg')
# When chrome is built with USE="pgo_generate", rootfs chrome is actually a
# symlink to a real binary which is in the stateful partition. So we do not
# check for a valid chrome binary in that case.
if (not self._IsPackageMerged('chromeos-base/chromeos-chrome[pgo_generate]')
and self._IsPackageMerged('chromeos-base/chromeos-chrome')):
binaries.append('opt/google/chrome/chrome')
binaries = [os.path.join(ROOT_A, x) for x in binaries]
# Grab all .so files
libraries = []
for root, _, files in os.walk(ROOT_A):
for name in files:
filename = os.path.join(root, name)
if '.so' in filename:
libraries.append(filename)
# lddtree is only available in the chroot but this image_test module
# is imported by test_stages outside of the chroot too. So we dynamically
# import lddtree here, where it actually is used.
import lddtree
ldpaths = lddtree.LoadLdpaths(ROOT_A)
for to_test in itertools.chain(binaries, libraries):
# to_test could be a symlink, we need to resolve it relative to ROOT_A.
while os.path.islink(to_test):
link = os.readlink(to_test)
if link.startswith('/'):
to_test = os.path.join(ROOT_A, link[1:])
else:
to_test = os.path.join(os.path.dirname(to_test), link)
try:
lddtree.ParseELF(to_test, ROOT_A, ldpaths)
except lddtree.exceptions.ELFError:
continue
except IOError as e:
self.fail('Fail linkage test for %s: %s' % (to_test, e))