blob: 40dbae9854f7e8b9a04c472838028181916d5680 [file] [log] [blame]
# Copyright 2020 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.
"""Test to validate servo.py log grabbing works."""
import logging
import os
import re
import tempfile
import shutil
from autotest_lib.client.common_lib import error
from autotest_lib.server import test
class servo_LogGrab(test.test):
"""Ensure all logs are grabbed and glued together."""
version = 1
# Regex used to extract log-record timestamp from log-line
# An example log record timestamp is:
# '2020-02-05 12:39:28,394 - EC - EC3PO.Interpreter'[..]
# This regex does a loose check on the date format as it does not
# enforce real month numbers for example.
# Note that the millisecond component is not part of the match group.
# This is because it can happen that two log messages compete for the
# file-print and one that was recorded a few milliseconds prior still
# makes it to the file first.
TS_RE = r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d{3} .*$'
TS_EXTRACTOR = re.compile(TS_RE)
def setup_dir(self):
"""Setup a test directory to grab servo logs into and out of."""
self.logtestdir = tempfile.mkdtemp()
def grab_and_get_directory(self):
"""Thin wrapper around servo's |grab_logs()| function.
@returns: directory name inside |self.logtestdir| containing servod logs
None if no directory was created (and logs grabbed)
@raises error.TestFail: if log grabbing creates more or less than one
directory
"""
self.servo_host.grab_logs(self.logtestdir)
dirs_created = len(os.listdir(self.logtestdir))
if dirs_created > 1:
raise error.TestFail('Log grabbing created more than one directory')
if dirs_created == 0:
# None to indicate no logs were grabbed.
return None
return os.path.join(self.logtestdir, os.listdir(self.logtestdir)[0])
def check_logs_ordered(self, outdir):
"""Helper to check that all logs in |outdir| are concat in order.
Log grabbing glues together all logs for one loglevel into one
file. These files ought to be in order.
@param outdir: directory with logs to inspect
@raises error.TestFail: if any log in |outdir| contains unsorted log
lines
"""
for logf in os.listdir(outdir):
if not logf.startswith(self.servo_host.JOINT_LOG_PREFIX):
# MCU files are to be skipped.
continue
logpath = os.path.join(outdir, logf)
timestamps = []
with open(logpath, 'r') as log:
for line in log:
ts_match = self.TS_EXTRACTOR.match(line)
if ts_match:
# It's safe to skip lines without timestamps, as those
# are just linebreaks in the log record.
timestamps.append(ts_match.group(1))
if timestamps != sorted(timestamps):
raise error.TestFail('Logfile %s contained unsorted '
'timestamps.' % logf)
def check_get_control_in_logs(self, outdir, ctrls):
"""Helper to check debug logs in |outdir|.
This method always checks to ensure that the init logline saying that
servod is listening on a port is printed.
Note: if a control has been issued n times, and the goal is to ensure
that it shows up n times in the logs, add it n times to |ctrls|.
@param outdir: directory with logs to inspect
@param ctrls: list of get controls that have been issued and should be
checked for
@returns: False: - if any of the controls in |ctrls| are not found in
the logs
- if the log line saying that servod is listening on a
port is missing
True: otherwise
"""
artifacts = [re.compile(r'get - name\(%s\)' % ctrl) for ctrl in ctrls]
# We also need to ensure that the initial logs don't get lost.
init_artifact = re.compile('Listening on (\d{1,3}\.){3}\d{1,3} '
'port \d{1,5}')
artifacts.append(init_artifact)
fname = os.path.join(outdir, 'log.DEBUG.txt')
with open(fname, 'r') as f:
for line in f:
# Only maintain the artifacts that have had no match yet.
new_artifacts = []
found = False
for artifact in artifacts:
if not found and artifact.search(line):
# Each artifact is for one log-line. If a line is
# supposed to appear x times, there should be x
# artifacts, therefore we need to continue to avoid
# undercounting.
found = True
continue
# No match, add back to list.
new_artifacts.append(artifact)
if not new_artifacts:
# All found, so the log file checks out.
return True
artifacts = new_artifacts
logging.error('The following artifacts were not found in logs:')
for artifact in artifacts:
logging.error(artifact.pattern)
return False
def test_singular(self):
"""Subtest to verify single-file grabbing works.
As for any new test, the servod instance should only have one file
upon initialization. This subtest ensures that the one file is grabbed
properly and found in the results directory.
@raises error.TestFail: if the log line saying that servod is listening
on a port is missing
@raises error.TestFail: one of the loglevel summaries is missing
"""
outdir = self.grab_and_get_directory()
required = ['log.%s.txt' % l for l in ['INFO', 'DEBUG', 'WARNING']]
logs = os.listdir(outdir)
for log in required:
if log not in logs:
raise error.TestFail('Required file %s missing. '
'Log files found: %s'
% (log, ', '.join(logs)))
self.check_logs_ordered(outdir)
self.check_get_control_in_logs(outdir, [])
def test_dual(self):
"""Subtest to verify singular file rotation works.
Test rotates the logs once and then checks integrity.
@raises error.TestFail: if the log line saying that servod is listening
on a port is missing
@raises error.TestFail: logline showing 'config_files' was issued is
missing
"""
logging.info('Running subtest to rotate logs once and validate that '
'logs are glued together in the results directory '
'correctly.')
# First, rotate out the logs.
self.servo.set_nocheck('rotate_servod_logs', 'yes')
# Next issue a control that (a) requires no underlying hardware, and
# (b) is not part of hwinit.
ctrl = 'config_files'
self.servo.get(ctrl)
outdir = self.grab_and_get_directory()
self.check_logs_ordered(outdir)
if not self.check_get_control_in_logs(outdir, [ctrl]):
raise error.TestFail('joint logfile log.DEBUG.txt does not contain '
'log artifact that was created after the first'
' rotation')
def test_compressed(self):
"""Subtest to verify multi file rotation and compression works.
Test rotates the logs many times to generate compression. Validate
that the output is properly decompressed with all expected lines.
@raises error.TestFail: if the log line saying that servod is listening
on a port is missing
@raises error.TestFail: logline showing 'config_files' was issued is
missing
@raises error.TestFail: any of the expected number of loglines showing
'servod_pid' was issued is missing
"""
logging.info('Running subtest to rotate logs past the compression '
'threshold and validate that compressed logs are '
'correctly decompressed upon grabbing.')
# First, rotate out the logs.
self.servo.set_nocheck('rotate_servod_logs', 'yes')
ctrl = 'config_files'
# Next issue a control that (a) requires no underlying hardware, and
# (b) is not part of hwinit.
self.servo.get(ctrl)
ctrls = [ctrl]
# Then, rotate out the logs and generate traffic for 6 additional times.
# That should trigger file compression.
# The command here has to be different to ensure that the first file
# that had the config_files in it got decompressed.
rot_ctrl = 'servod_pid'
for _ in range(6):
self.servo.set_nocheck('rotate_servod_logs', 'yes')
self.servo.get(rot_ctrl)
# Append every time to ensure it shows up the exact number of
# times in the logs.
ctrls.append(rot_ctrl)
outdir = self.grab_and_get_directory()
self.check_logs_ordered(outdir)
if not self.check_get_control_in_logs(outdir, ctrls):
raise error.TestFail('joint logfile log.DEBUG.txt does not contain '
'log artifact that was created after the '
'multiple rotation that leads to compression.')
def test_missing(self):
"""Subtest to verify missing symlinks will grab no logs, throw no error.
@raises error.TestFail: deleting symlinks to the latest logs causes
logs to still be grabbed
"""
logging.info('Running subtest to remove latest log symlinks and '
'validate this results in a grab logs noop.')
self.servo_host.remove_latest_log_symlinks()
outdir = self.grab_and_get_directory()
if not outdir == None:
raise error.TestFail('Removing symlinks to latest logs still '
'caused test to find logs.')
def run_once(self, host):
"""Try 3 scenarios of log grabbing: single, multiple, compressed."""
self.servo_host = host._servo_host
self.servo = host.servo
# Note that test_missing should run last as it is destructive and
# removes logs on the servo_host side. Alternatively, restart
# servod if another destructive test is added.
for subtest in [self.test_singular, self.test_dual,
self.test_compressed, self.test_missing]:
self.setup_dir()
subtest()
logging.info('Success')
self.cleanup_logs()
def cleanup_logs(self):
"""Delete the temporary directory."""
shutil.rmtree(self.logtestdir)