• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Test to validate servo.py log grabbing works."""
6
7import logging
8import os
9import re
10import tempfile
11import shutil
12
13from autotest_lib.client.common_lib import error
14from autotest_lib.server import test
15
16class servo_LogGrab(test.test):
17    """Ensure all logs are grabbed and glued together."""
18    version = 1
19
20    # Regex used to extract log-record timestamp from log-line
21    # An example log record timestamp is:
22    #   '2020-02-05 12:39:28,394 - EC - EC3PO.Interpreter'[..]
23    # This regex does a loose check on the date format as it does not
24    # enforce real month numbers for example.
25    # Note that the millisecond component is not part of the match group.
26    # This is because it can happen that two log messages compete for the
27    # file-print and one that was recorded a few milliseconds prior still
28    # makes it to the file first.
29    TS_RE = r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d{3} .*$'
30
31    TS_EXTRACTOR = re.compile(TS_RE)
32
33    def setup_dir(self):
34        """Setup a test directory to grab servo logs into and out of."""
35        self.logtestdir = tempfile.mkdtemp()
36
37    def grab_and_get_directory(self):
38        """Thin wrapper around servo_host instance_logs function.
39
40        @returns: directory name inside |self.logtestdir| containing servod logs
41                  None if no directory was created (and logs grabbed)
42
43        @raises error.TestFail: if log grabbing creates more or less than one
44                                directory
45        """
46        ts = self.servo_host.get_instance_logs_ts()
47        self.servo_host.get_instance_logs(ts, self.logtestdir)
48        dirs_created = len(os.listdir(self.logtestdir))
49        if dirs_created > 1:
50            raise error.TestFail('Log grabbing created more than one directory')
51        if dirs_created == 0:
52            # None to indicate no logs were grabbed.
53            return None
54        return os.path.join(self.logtestdir, os.listdir(self.logtestdir)[0])
55
56    def check_logs_ordered(self, outdir):
57        """Helper to check that all logs in |outdir| are concat in order.
58
59        Log grabbing glues together all logs for one loglevel into one
60        file. These files ought to be in order.
61
62        @param outdir: directory with logs to inspect
63
64        @raises error.TestFail: if any log in |outdir| contains unsorted log
65                                lines
66        """
67        for logf in os.listdir(outdir):
68            if not logf.startswith(self.servo_host.JOINT_LOG_PREFIX):
69                # MCU files are to be skipped.
70                continue
71            logpath = os.path.join(outdir, logf)
72            timestamps = []
73            with open(logpath, 'r') as log:
74                for line in log:
75                    ts_match = self.TS_EXTRACTOR.match(line)
76                    if ts_match:
77                        # It's safe to skip lines without timestamps, as those
78                        # are just linebreaks in the log record.
79                        timestamps.append(ts_match.group(1))
80            if timestamps != sorted(timestamps):
81                raise error.TestFail('Logfile %s contained unsorted '
82                                     'timestamps.' % logf)
83
84    def check_get_control_in_logs(self, outdir, ctrls):
85        """Helper to check debug logs in |outdir|.
86
87        This method always checks to ensure that the init logline saying that
88        servod is listening on a port is printed.
89
90        Note: if a control has been issued n times, and the goal is to ensure
91        that it shows up n times in the logs, add it n times to |ctrls|.
92
93        @param outdir: directory with logs to inspect
94        @param ctrls: list of get controls that have been issued and should be
95                      checked for
96
97        @returns: False: - if any of the controls in |ctrls| are not found in
98                           the logs
99                         - if the log line saying that servod is listening on a
100                           port is missing
101                  True: otherwise
102        """
103        artifacts = [re.compile(r'- \(get\) %s' % ctrl) for ctrl in ctrls]
104        # We also need to ensure that the initial logs don't get lost.
105        init_artifact = re.compile('Listening on (\d{1,3}\.){3}\d{1,3} '
106                                   'port \d{1,5}')
107        artifacts.append(init_artifact)
108        fname = os.path.join(outdir, 'log.DEBUG.txt')
109        with open(fname, 'r') as f:
110            for line in f:
111                # Only maintain the artifacts that have had no match yet.
112                new_artifacts = []
113                found = False
114                for artifact in artifacts:
115                    if not found and artifact.search(line):
116                        # Each artifact is for one log-line. If a line is
117                        # supposed to appear x times, there should be x
118                        # artifacts, therefore we need to continue to avoid
119                        # undercounting.
120                        found = True
121                        continue
122                    # No match, add back to list.
123                    new_artifacts.append(artifact)
124                if not new_artifacts:
125                    # All found, so the log file checks out.
126                    return True
127                artifacts = new_artifacts
128        logging.error('The following artifacts were not found in logs:')
129        for artifact in artifacts:
130            logging.error(artifact.pattern)
131        return False
132
133
134    def test_singular(self):
135        """Subtest to verify single-file grabbing works.
136
137        As for any new test, the servod instance should only have one file
138        upon initialization. This subtest ensures that the one file is grabbed
139        properly and found in the results directory.
140
141        @raises error.TestFail: if the log line saying that servod is listening
142                                on a port is missing
143        @raises error.TestFail: one of the loglevel summaries is missing
144        """
145        outdir = self.grab_and_get_directory()
146        required = ['log.%s.txt' % l for l in ['INFO', 'DEBUG', 'WARNING']]
147        logs = os.listdir(outdir)
148        for log in required:
149            if log not in logs:
150                raise error.TestFail('Required file %s missing. '
151                                     'Log files found: %s'
152                                     % (log, ', '.join(logs)))
153        self.check_logs_ordered(outdir)
154        self.check_get_control_in_logs(outdir, [])
155
156    def test_dual(self):
157        """Subtest to verify singular file rotation works.
158
159        Test rotates the logs once and then checks integrity.
160
161        @raises error.TestFail: if the log line saying that servod is listening
162                                on a port is missing
163        @raises error.TestFail: logline showing 'config_files' was issued is
164                                missing
165        """
166        logging.info('Running subtest to rotate logs once and validate that '
167                     'logs are glued together in the results directory '
168                     'correctly.')
169        # First, rotate out the logs.
170        self.servo.set_nocheck('rotate_servod_logs', 'yes')
171        # Next issue a control that (a) requires no underlying hardware, and
172        # (b) is not part of hwinit.
173        ctrl = 'config_files'
174        self.servo.get(ctrl)
175        outdir = self.grab_and_get_directory()
176        self.check_logs_ordered(outdir)
177        if not self.check_get_control_in_logs(outdir, [ctrl]):
178            raise error.TestFail('joint logfile log.DEBUG.txt does not contain '
179                                 'log artifact that was created after the first'
180                                 ' rotation')
181
182    def test_compressed(self):
183        """Subtest to verify multi file rotation and compression works.
184
185        Test rotates the logs many times to generate compression. Validate
186        that the output is properly decompressed with all expected lines.
187
188        @raises error.TestFail: if the log line saying that servod is listening
189                                on a port is missing
190        @raises error.TestFail: logline showing 'config_files' was issued is
191                                missing
192        @raises error.TestFail: any of the expected number of loglines showing
193                                'servod_pid' was issued is missing
194        """
195        logging.info('Running subtest to rotate logs past the compression '
196                     'threshold and validate that compressed logs are '
197                     'correctly decompressed upon grabbing.')
198        # First, rotate out the logs.
199        self.servo.set_nocheck('rotate_servod_logs', 'yes')
200        ctrl = 'config_files'
201        # Next issue a control that (a) requires no underlying hardware, and
202        # (b) is not part of hwinit.
203        self.servo.get(ctrl)
204        ctrls = [ctrl]
205        # Then, rotate out the logs and generate traffic for 6 additional times.
206        # That should trigger file compression.
207        # The command here has to be different to ensure that the first file
208        # that had the config_files in it got decompressed.
209        rot_ctrl = 'servod_pid'
210        for _ in range(6):
211            self.servo.set_nocheck('rotate_servod_logs', 'yes')
212            self.servo.get(rot_ctrl)
213            # Append every time to ensure it shows up the exact number of
214            # times in the logs.
215            ctrls.append(rot_ctrl)
216        outdir = self.grab_and_get_directory()
217        self.check_logs_ordered(outdir)
218        if not self.check_get_control_in_logs(outdir, ctrls):
219            raise error.TestFail('joint logfile log.DEBUG.txt does not contain '
220                                 'log artifact that was created after the '
221                                 'multiple rotation that leads to compression.')
222
223    def test_missing(self):
224        """Subtest to verify missing symlinks will grab no logs, throw no error.
225
226        @raises error.TestFail: deleting symlinks to the latest logs causes
227                                logs to still  be grabbed
228        """
229        logging.info('Running subtest to remove latest log symlinks and '
230                     'validate this results in a grab logs noop.')
231        self.servo_host.remove_latest_log_symlinks()
232        outdir = self.grab_and_get_directory()
233        if not outdir == None:
234            raise error.TestFail('Removing symlinks to latest logs still '
235                                 'caused test to find logs.')
236
237    def test_restart(self):
238        """Subtest to verify that a servod restart in the middle is caught.
239
240        @raises error.TestFail: if the logs from the previous instances are
241                                missing on a restart
242        """
243        restarts = 2
244        # Need to cache the initial timestamp so that we can reuse it later
245        initial_ts = self.servo_host.get_instance_logs_ts()
246        for _ in range(restarts):
247            self.servo_host.restart_servod()
248        self.servo_host._initial_instance_ts = initial_ts
249        # Grab the logs and probe for old, orphaned logs
250        ts = self.servo_host.get_instance_logs_ts()
251        self.servo_host.probe_servod_restart(ts, self.logtestdir)
252        self.servo_host.get_instance_logs(ts, self.logtestdir)
253        # First, validate that in total 3 directories were created: one
254        # for the current instance, and two for the old ones
255        dirs_created = len(os.listdir(self.logtestdir))
256        expected_dirs = restarts + 1
257        if dirs_created != expected_dirs:
258            logging.error('Should have created %d dirs from grabbing logs '
259                          'but only created %d.', expected_dirs, dirs_created)
260            raise error.TestFail('Grabbing logs from restarted instance failed')
261        # Check that panicinfo was called on the MCUs
262        # TODO(coconutruben): refactor this into one source of truth on which
263        # servos support panicinfo
264        # There should only ever be one directory here that doesn't have the
265        # suffix but if that logic changes in servo_host, then this needs to be
266        # refactored, or an API call added to servo_host
267        final_instance_dir = [d for d in os.listdir(self.logtestdir) if
268                              self.servo_host.OLD_LOG_SUFFIX not in d][0]
269        final_instance_dir_path = os.path.join(self.logtestdir,
270                                               final_instance_dir)
271        for mcu in ['servo_v4', 'servo_micro']:
272            mcu_path = os.path.join(final_instance_dir_path, '%s.txt' % mcu)
273            if os.path.exists(mcu_path):
274                with open(mcu_path, 'r') as f:
275                    if 'panicinfo' not in f.read():
276                        raise error.TestFail('No panicinfo call found in %r.' %
277                                             mcu_path)
278    def test_restart_no_servo(self):
279        """Subtest to verify that lack of |_servo| attribute yields no failure.
280
281        @raises error.TestFail: if the lack of _servo causes an issue
282        """
283        self.servo_host.servo = None
284        try:
285            self.test_restart()
286        except AttributeError as e:
287            raise error.TestFail('Setting |_servo| to None caused an error. %s'
288                                 % str(e))
289        except error.TestFail as e:
290            if 'No panicinfo call found in' in str(e):
291                # This means that the lack of |_servo| worked as intended e.g.
292                # the logs are grabbed, but the panicinfo could not be issued.
293                pass
294            # for any other issue, we need to actually raise it.
295            raise e
296        # Restore servo object again.
297        self.servo_host.servo = self.servo
298
299    def run_once(self, host):
300        """Try 3 scenarios of log grabbing: single, multiple, compressed."""
301        self.servo_host = host._servo_host
302        self.servo = host.servo
303        # Note that test_missing should run last as it is destructive and
304        # removes logs on the servo_host side. Alternatively, restart
305        # servod if another destructive test is added.
306        for subtest in [self.test_singular, self.test_dual,
307                        self.test_compressed, self.test_restart,
308                        self.test_restart_no_servo, self.test_missing]:
309            self.setup_dir()
310            subtest()
311            logging.info('Success')
312            self.cleanup_logs()
313
314    def cleanup_logs(self):
315        """Delete the temporary directory."""
316        shutil.rmtree(self.logtestdir)
317