| # 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_host instance_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 |
| """ |
| ts = self.servo_host.get_instance_logs_ts() |
| self.servo_host.get_instance_logs(ts, 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\) %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 test_restart(self): |
| """Subtest to verify that a servod restart in the middle is caught. |
| |
| @raises error.TestFail: if the logs from the previous instances are |
| missing on a restart |
| """ |
| restarts = 2 |
| # Need to cache the initial timestamp so that we can reuse it later |
| initial_ts = self.servo_host.get_instance_logs_ts() |
| for _ in range(restarts): |
| self.servo_host.restart_servod() |
| self.servo_host._initial_instance_ts = initial_ts |
| # Grab the logs and probe for old, orphaned logs |
| ts = self.servo_host.get_instance_logs_ts() |
| self.servo_host.probe_servod_restart(ts, self.logtestdir) |
| self.servo_host.get_instance_logs(ts, self.logtestdir) |
| # First, validate that in total 3 directories were created: one |
| # for the current instance, and two for the old ones |
| dirs_created = len(os.listdir(self.logtestdir)) |
| expected_dirs = restarts + 1 |
| if dirs_created != expected_dirs: |
| logging.error('Should have created %d dirs from grabbing logs ' |
| 'but only created %d.', expected_dirs, dirs_created) |
| raise error.TestFail('Grabbing logs from restarted instance failed') |
| # Check that panicinfo was called on the MCUs |
| # TODO(coconutruben): refactor this into one source of truth on which |
| # servos support panicinfo |
| # There should only ever be one directory here that doesn't have the |
| # suffix but if that logic changes in servo_host, then this needs to be |
| # refactored, or an API call added to servo_host |
| final_instance_dir = [d for d in os.listdir(self.logtestdir) if |
| self.servo_host.OLD_LOG_SUFFIX not in d][0] |
| final_instance_dir_path = os.path.join(self.logtestdir, |
| final_instance_dir) |
| for mcu in ['servo_v4', 'servo_micro']: |
| mcu_path = os.path.join(final_instance_dir_path, '%s.txt' % mcu) |
| if os.path.exists(mcu_path): |
| with open(mcu_path, 'r') as f: |
| if 'panicinfo' not in f.read(): |
| raise error.TestFail('No panicinfo call found in %r.' % |
| mcu_path) |
| def test_restart_no_servo(self): |
| """Subtest to verify that lack of |_servo| attribute yields no failure. |
| |
| @raises error.TestFail: if the lack of _servo causes an issue |
| """ |
| self.servo_host.servo = None |
| try: |
| self.test_restart() |
| except AttributeError as e: |
| raise error.TestFail('Setting |_servo| to None caused an error. %s' |
| % str(e)) |
| except error.TestFail as e: |
| if 'No panicinfo call found in' in str(e): |
| # This means that the lack of |_servo| worked as intended e.g. |
| # the logs are grabbed, but the panicinfo could not be issued. |
| pass |
| # for any other issue, we need to actually raise it. |
| raise e |
| # Restore servo object again. |
| self.servo_host.servo = self.servo |
| |
| 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_restart, |
| self.test_restart_no_servo, 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) |