blob: 6966923c94df6bb1a449cc837dc44cdbe389d12b [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
Patrick Williams92b42cb2022-09-03 06:53:57 -05002# Copyright OpenEmbedded Contributors
3#
Brad Bishopc342db32019-05-15 21:57:59 -04004# SPDX-License-Identifier: MIT
5#
6
Andrew Geissler220dafd2023-10-04 10:18:08 -05007import collections
Brad Bishop6e60e8b2018-02-01 10:27:11 -05008import os
Patrick Williams169d7bc2024-01-05 11:33:25 -06009import sys
Brad Bishop6e60e8b2018-02-01 10:27:11 -050010
Brad Bishop6e60e8b2018-02-01 10:27:11 -050011from shutil import rmtree
12from oeqa.runtime.case import OERuntimeTestCase
13from oeqa.core.decorator.depends import OETestDepends
Brad Bishop6e60e8b2018-02-01 10:27:11 -050014
Patrick Williams169d7bc2024-01-05 11:33:25 -060015# importlib.resources.open_text in Python <3.10 doesn't search all directories
16# when a package is split across multiple directories. Until we can rely on
17# 3.10+, reimplement the searching logic.
18if sys.version_info < (3, 10):
19 def _open_text(package, resource):
20 import importlib, pathlib
21 module = importlib.import_module(package)
22 for path in module.__path__:
23 candidate = pathlib.Path(path) / resource
24 if candidate.exists():
25 return candidate.open(encoding='utf-8')
26 raise FileNotFoundError
27else:
28 from importlib.resources import open_text as _open_text
Brad Bishop6e60e8b2018-02-01 10:27:11 -050029
Brad Bishop6e60e8b2018-02-01 10:27:11 -050030
Brad Bishop6e60e8b2018-02-01 10:27:11 -050031class ParseLogsTest(OERuntimeTestCase):
32
Andrew Geissler220dafd2023-10-04 10:18:08 -050033 # Which log files should be collected
34 log_locations = ["/var/log/", "/var/log/dmesg", "/tmp/dmesg_output.log"]
35
36 # The keywords that identify error messages in the log files
37 errors = ["error", "cannot", "can't", "failed"]
38
Patrick Williams169d7bc2024-01-05 11:33:25 -060039 # A list of error messages that should be ignored
40 ignore_errors = []
41
Brad Bishop6e60e8b2018-02-01 10:27:11 -050042 @classmethod
43 def setUpClass(cls):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050044 # When systemd is enabled we need to notice errors on
45 # circular dependencies in units.
Andrew Geissler220dafd2023-10-04 10:18:08 -050046 if 'systemd' in cls.td.get('DISTRO_FEATURES'):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050047 cls.errors.extend([
48 'Found ordering cycle on',
49 'Breaking ordering cycle by deleting job',
50 'deleted to break ordering cycle',
51 'Ordering cycle found, skipping',
52 ])
53
Andrew Geissler220dafd2023-10-04 10:18:08 -050054 cls.errors = [s.casefold() for s in cls.errors]
Brad Bishop6e60e8b2018-02-01 10:27:11 -050055
Patrick Williams169d7bc2024-01-05 11:33:25 -060056 cls.load_machine_ignores()
57
58 @classmethod
59 def load_machine_ignores(cls):
60 # Add TARGET_ARCH explicitly as not every machine has that in MACHINEOVERRDES (eg qemux86-64)
61 for candidate in ["common", cls.td.get("TARGET_ARCH")] + cls.td.get("MACHINEOVERRIDES").split(":"):
62 try:
63 name = f"parselogs-ignores-{candidate}.txt"
64 for line in _open_text("oeqa.runtime.cases", name):
65 line = line.strip()
66 if line and not line.startswith("#"):
67 cls.ignore_errors.append(line.casefold())
68 except FileNotFoundError:
69 pass
Brad Bishop6e60e8b2018-02-01 10:27:11 -050070
71 # Go through the log locations provided and if it's a folder
72 # create a list with all the .log files in it, if it's a file
73 # just add it to that list.
74 def getLogList(self, log_locations):
75 logs = []
76 for location in log_locations:
Andrew Geissler220dafd2023-10-04 10:18:08 -050077 status, _ = self.target.run('test -f %s' % location)
Brad Bishop6e60e8b2018-02-01 10:27:11 -050078 if status == 0:
Andrew Geissler220dafd2023-10-04 10:18:08 -050079 logs.append(location)
Brad Bishop6e60e8b2018-02-01 10:27:11 -050080 else:
Andrew Geissler220dafd2023-10-04 10:18:08 -050081 status, _ = self.target.run('test -d %s' % location)
Brad Bishop6e60e8b2018-02-01 10:27:11 -050082 if status == 0:
Andrew Geissler220dafd2023-10-04 10:18:08 -050083 cmd = 'find %s -name \\*.log -maxdepth 1 -type f' % location
Brad Bishop6e60e8b2018-02-01 10:27:11 -050084 status, output = self.target.run(cmd)
85 if status == 0:
86 output = output.splitlines()
87 for logfile in output:
Andrew Geissler220dafd2023-10-04 10:18:08 -050088 logs.append(os.path.join(location, logfile))
Brad Bishop6e60e8b2018-02-01 10:27:11 -050089 return logs
90
91 # Copy the log files to be parsed locally
92 def transfer_logs(self, log_list):
Andrew Geissler220dafd2023-10-04 10:18:08 -050093 workdir = self.td.get('WORKDIR')
Brad Bishop6e60e8b2018-02-01 10:27:11 -050094 self.target_logs = workdir + '/' + 'target_logs'
95 target_logs = self.target_logs
96 if os.path.exists(target_logs):
97 rmtree(self.target_logs)
98 os.makedirs(target_logs)
99 for f in log_list:
100 self.target.copyFrom(str(f), target_logs)
101
102 # Get the local list of logs
103 def get_local_log_list(self, log_locations):
104 self.transfer_logs(self.getLogList(log_locations))
105 list_dir = os.listdir(self.target_logs)
106 dir_files = [os.path.join(self.target_logs, f) for f in list_dir]
107 logs = [f for f in dir_files if os.path.isfile(f)]
108 return logs
109
Andrew Geissler220dafd2023-10-04 10:18:08 -0500110 def get_context(self, lines, index, before=6, after=3):
111 """
112 Given a set of lines and the index of the line that is important, return
113 a number of lines surrounding that line.
114 """
115 last = len(lines)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500116
Andrew Geissler220dafd2023-10-04 10:18:08 -0500117 start = index - before
118 end = index + after + 1
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500119
Andrew Geissler220dafd2023-10-04 10:18:08 -0500120 if start < 0:
121 end -= start
122 start = 0
123 if end > last:
124 start -= end - last
125 end = last
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500126
Andrew Geissler220dafd2023-10-04 10:18:08 -0500127 return lines[start:end]
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500128
Andrew Geissler220dafd2023-10-04 10:18:08 -0500129 def test_get_context(self):
130 """
131 A test case for the test case.
132 """
133 lines = list(range(0,10))
134 self.assertEqual(self.get_context(lines, 0, 2, 1), [0, 1, 2, 3])
135 self.assertEqual(self.get_context(lines, 5, 2, 1), [3, 4, 5, 6])
136 self.assertEqual(self.get_context(lines, 9, 2, 1), [6, 7, 8, 9])
137
138 def parse_logs(self, logs, lines_before=10, lines_after=10):
139 """
140 Search the log files @logs looking for error lines (marked by
141 @self.errors), ignoring anything listed in @self.ignore_errors.
142
143 Returns a dictionary of log filenames to a dictionary of error lines to
144 the error context (controlled by @lines_before and @lines_after).
145 """
146 results = collections.defaultdict(dict)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500147
148 for log in logs:
Andrew Geissler220dafd2023-10-04 10:18:08 -0500149 with open(log) as f:
150 lines = f.readlines()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500151
Andrew Geissler220dafd2023-10-04 10:18:08 -0500152 for i, line in enumerate(lines):
153 line = line.strip()
154 line_lower = line.casefold()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500155
Andrew Geissler220dafd2023-10-04 10:18:08 -0500156 if any(keyword in line_lower for keyword in self.errors):
157 if not any(ignore in line_lower for ignore in self.ignore_errors):
158 results[log][line] = "".join(self.get_context(lines, i, lines_before, lines_after))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500159
160 return results
161
162 # Get the output of dmesg and write it in a file.
163 # This file is added to log_locations.
164 def write_dmesg(self):
165 (status, dmesg) = self.target.run('dmesg > /tmp/dmesg_output.log')
166
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500167 @OETestDepends(['ssh.SSHTest.test_ssh'])
168 def test_parselogs(self):
169 self.write_dmesg()
170 log_list = self.get_local_log_list(self.log_locations)
Andrew Geissler220dafd2023-10-04 10:18:08 -0500171 result = self.parse_logs(log_list)
172
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500173 errcount = 0
Andrew Geissler220dafd2023-10-04 10:18:08 -0500174 self.msg = ""
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500175 for log in result:
176 self.msg += 'Log: ' + log + '\n'
177 self.msg += '-----------------------\n'
178 for error in result[log]:
179 errcount += 1
Andrew Geissler220dafd2023-10-04 10:18:08 -0500180 self.msg += 'Central error: ' + error + '\n'
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500181 self.msg += '***********************\n'
Andrew Geissler220dafd2023-10-04 10:18:08 -0500182 self.msg += result[log][error] + '\n'
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500183 self.msg += '***********************\n'
184 self.msg += '%s errors found in logs.' % errcount
185 self.assertEqual(errcount, 0, msg=self.msg)