blob: 4b596a0d49a5ad482a3844e1d8ce1fc2efbd6fe5 [file] [log] [blame]
Struan Shrimptona64cb192024-03-05 02:22:001# Copyright 2024 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Struan Shrimptond98829f2024-03-12 19:41:384"""Adapter for printing legacy recipe output
Struan Shrimptona64cb192024-03-05 02:22:005
Struan Shrimptond98829f2024-03-12 19:41:386TODO(https://crbug.com/326904531): This file is intended to be a temporary
7workaround and should be replaced once this bug is resolved"""
8
9import json
Struan Shrimptona64cb192024-03-05 02:22:0010import logging
Struan Shrimpton33065b92024-11-15 23:00:1511import os
Struan Shrimptona64cb192024-03-05 02:22:0012import re
13import sys
14
Struan Shrimpton3b28ff02024-04-03 22:46:0515# Create a logger that avoids rich formatting as we can't control recipe
16# formatting from here
17basic_logger = logging.getLogger('basic_logger')
18basic_logger.addHandler(logging.StreamHandler(sys.stdout))
19basic_logger.propagate = False
Struan Shrimptona64cb192024-03-05 02:22:0020
Ben Pasteneca5ad4b2024-03-05 19:02:4721class PassthroughAdapter:
22 """Doesn't filter anything, just logs everything from the recipe run."""
23
24 def ProcessLine(self, line):
Struan Shrimpton3b28ff02024-04-03 22:46:0525 basic_logger.log(logging.DEBUG, line)
Ben Pasteneca5ad4b2024-03-05 19:02:4726
27
Struan Shrimptona64cb192024-03-05 02:22:0028class LegacyOutputAdapter:
Struan Shrimptond98829f2024-03-12 19:41:3829 """Interprets the legacy recipe run mode output to logging
30
31 This will filter, route and in some cases reformat the output to trace levels
32 of logging. This will cause specific output (e.g. unfiltered step names) to
33 always print to std out or when -v is passed, the stdout will additionally
34 be passed to the logging stdout. Note -vv will cause PassthroughAdapter to
35 interpret results"""
36
37 SEED_STEP_TEXT = '@@@SEED_STEP@'
38 STEP_CLOSED_TEXT = '@@@STEP_CLOSED@@@'
39 ANNOTATOR_PREFIX_SUFIX = '@@@'
40 TRIGGER_STEP_PREFIX = 'test_pre_run.[trigger] '
41 TRIGGER_LINK_TEXT = '@@@STEP_LINK@task UI:'
Struan Shrimptona64cb192024-03-05 02:22:0042
43 def __init__(self):
Struan Shrimptond98829f2024-03-12 19:41:3844 self._trigger_link_re = re.compile(r'.+@(https://.+)@@@$')
Struan Shrimptona64cb192024-03-05 02:22:0045 self._ninja_status_re = re.compile(r'\[(\d+)\/(\d+)\]')
Struan Shrimptond98829f2024-03-12 19:41:3846 self._collect_wait_re = re.compile(
47 r'.+prpc call (.+) swarming.v2.Tasks.ListTaskStates, stdin: '
48 r'(\{"task_id": .+\})$'
49 )
50 self._result_links_re = re.compile(
51 r'@@@STEP_LINK@shard (#\d+) test results@(https://[^@]+)@@@')
Ben Pastene37651fc502024-03-19 22:32:4152
Struan Shrimptond98829f2024-03-12 19:41:3853 self._current_proccess_fn = self._StepNameProcessLine
Struan Shrimpton5267e0f2024-03-28 22:22:2654 # The first match is used. This allows us to filter parent steps while still
55 # printing child steps by adding the child step name first. By default
56 # _StepNameProcessLine will be used which prints the step name and it's
57 # stdout
Struan Shrimptona64cb192024-03-05 02:22:0058 self._step_to_processors = {
Ben Pasteneca5ad4b2024-03-05 19:02:4759 'compile': self._ProcessCompileLine,
60 'reclient compile': self._ProcessCompileLine,
Struan Shrimptond98829f2024-03-12 19:41:3861 'test_pre_run.[trigger] ': self._ProcessTriggerLine,
62 'collect tasks.wait for tasks': self._ProcessCollectLine,
Struan Shrimptona64cb192024-03-05 02:22:0063 }
Struan Shrimpton5267e0f2024-03-28 22:22:2664 # The first match is used. This allows us to filter parent steps while still
65 # printing child steps by adding the child step name first. By default INFO
66 # will be used which prints in non-verbose mode (i.e. no -v flag)
Struan Shrimptona64cb192024-03-05 02:22:0067 self._step_to_log_level = {
Struan Shrimpton5267e0f2024-03-28 22:22:2668 'lookup_builder_gn_args': logging.DEBUG,
69 'git rev-parse': logging.DEBUG,
70 'git diff to instrument': logging.DEBUG,
71 'save paths of affected files': logging.DEBUG,
72 'preprocess for reclient.start reproxy via bootstrap': logging.INFO,
73 'preprocess for reclient': logging.DEBUG,
74 'process clang crashes': logging.DEBUG,
75 'compile confirm no-op': logging.DEBUG,
76 'postprocess for reclient': logging.DEBUG,
Struan Shrimptond98829f2024-03-12 19:41:3877 'setup_build': logging.DEBUG,
78 'get compile targets for scripts': logging.DEBUG,
79 'lookup GN args': logging.DEBUG,
80 'install infra/tools/luci/isolate': logging.DEBUG,
81 'find command lines': logging.DEBUG,
82 'test_pre_run.install infra/tools/luci/swarming': logging.DEBUG,
83 'isolate tests': logging.DEBUG,
84 'read GN args': logging.DEBUG,
85 'test_pre_run.[trigger] ': logging.INFO,
86 'test_pre_run.': logging.DEBUG,
87 'collect tasks.wait for tasks': logging.INFO,
88 'collect tasks': logging.DEBUG,
89 '$debug - all results': logging.DEBUG,
90 'Test statistics': logging.DEBUG,
91 'read gclient': logging.DEBUG,
92 'write output_properties_file': logging.DEBUG,
Struan Shrimptona64cb192024-03-05 02:22:0093 }
Struan Shrimptona64cb192024-03-05 02:22:0094 # Setup logger for printing to the same line
95 logger = logging.getLogger('single_line_logger')
96 handler = logging.StreamHandler(sys.stdout)
97 handler.terminator = ''
98 logger.addHandler(handler)
99 logger.propagate = False
Ben Pastene37651fc502024-03-19 22:32:41100
101 self._last_line = ''
Struan Shrimpton33065b92024-11-15 23:00:15102 self._last_line_teriminal_lines = 0
Ben Pastene37651fc502024-03-19 22:32:41103 self._current_log_level = logging.DEBUG
Struan Shrimptona64cb192024-03-05 02:22:00104 self._single_line_logger = logger
Struan Shrimpton33065b92024-11-15 23:00:15105 self._terminal_columns, _ = os.get_terminal_size()
Struan Shrimptond98829f2024-03-12 19:41:38106 self._current_step_name = ''
107 self._dot_count = 0
Struan Shrimptona64cb192024-03-05 02:22:00108
Struan Shrimptond98829f2024-03-12 19:41:38109 def _StdoutProcessLine(self, line):
110 if not line.startswith(self.ANNOTATOR_PREFIX_SUFIX):
Struan Shrimptona64cb192024-03-05 02:22:00111 # Pass through any non-engine text
Ben Pastene2446d70a2024-07-15 21:44:16112 is_urlish = re.match(r'^http[s]?://\S+$', line)
113 if is_urlish:
114 logging.log(self._current_log_level, line)
115 else:
116 basic_logger.log(self._current_log_level, line)
Struan Shrimptona64cb192024-03-05 02:22:00117
Struan Shrimptond98829f2024-03-12 19:41:38118 def _StepNameProcessLine(self, line):
119 if line.startswith(self.SEED_STEP_TEXT):
120 # Always print the step name to info
121 logging.log(self._current_log_level,
Struan Shrimpton3b28ff02024-04-03 22:46:05122 '\n[cyan]Running: ' + self._current_step_name + '[/]')
Struan Shrimptond98829f2024-03-12 19:41:38123 return
Ben Pastene2446d70a2024-07-15 21:44:16124 self._StdoutProcessLine(line)
Struan Shrimptond98829f2024-03-12 19:41:38125
126 def _ProcessTriggerLine(self, line):
127 if line.startswith(self.SEED_STEP_TEXT + self.TRIGGER_STEP_PREFIX):
128 # The step names for tests don't have any identifying keywords so the
129 # result step parsers need to be installed at trigger time
130 test_name = line[len(self.SEED_STEP_TEXT +
131 self.TRIGGER_STEP_PREFIX):line.index(' (') if ' (' in
132 line else -len(self.ANNOTATOR_PREFIX_SUFIX)]
133 self._step_to_processors[test_name] = self._ProcessResult
134 self._step_to_log_level[test_name] = logging.DEBUG
135 elif line.startswith(self.TRIGGER_LINK_TEXT):
136 matches = self._trigger_link_re.match(line)
137 if matches:
138 task_name = self._current_step_name[len(self.TRIGGER_STEP_PREFIX):]
Struan Shrimpton3b28ff02024-04-03 22:46:05139 basic_logger.log(self._current_log_level,
140 f'Triggered {task_name}: ' + matches[1])
Struan Shrimptond98829f2024-03-12 19:41:38141 else:
142 self._StdoutProcessLine(line)
143
Ben Pasteneca5ad4b2024-03-05 19:02:47144 def _ProcessCompileLine(self, line):
Struan Shrimptond98829f2024-03-12 19:41:38145 if line.startswith(self.SEED_STEP_TEXT):
Struan Shrimpton3b28ff02024-04-03 22:46:05146 logging.info('\n[cyan]Running: ' + self._current_step_name + '[/]')
Struan Shrimptond98829f2024-03-12 19:41:38147 return
Struan Shrimptona64cb192024-03-05 02:22:00148 matches = self._ninja_status_re.match(line)
149 if matches:
Struan Shrimpton33065b92024-11-15 23:00:15150 # Remove the last line which might be multiple on the terminal
151 self._single_line_logger.log(self._current_log_level, '\33[2K')
152 if self._last_line_teriminal_lines > 1:
153 for _ in range(self._last_line_teriminal_lines - 1):
154 self._single_line_logger.log(self._current_log_level, '\33[A\33[2K')
155 self._single_line_logger.log(self._current_log_level, '\r' + line)
156 self._single_line_logger.handlers[0].flush()
Struan Shrimptona64cb192024-03-05 02:22:00157 return
Ben Pasteneff8e1f122024-03-22 17:03:16158 if self._last_line.startswith('['):
Struan Shrimpton3b28ff02024-04-03 22:46:05159 basic_logger.log(self._current_log_level, '')
Struan Shrimptond98829f2024-03-12 19:41:38160 self._StdoutProcessLine(line)
161
162 def _ProcessCollectLine(self, line):
163 if line.startswith(self.SEED_STEP_TEXT):
Struan Shrimpton3b28ff02024-04-03 22:46:05164 logging.info('\n[cyan]Running: ' + self._current_step_name + '[/]')
Struan Shrimptond98829f2024-03-12 19:41:38165 matches = self._collect_wait_re.match(line)
166 if matches:
167 task_ids = json.loads(matches[2])['task_id']
168 self._dot_count = (self._dot_count % 5) + 1
169 self._single_line_logger.log(
170 self._current_log_level,
171 f'\33[2K\rStill waiting on: {len(task_ids)} shard(s)' +
172 '.' * self._dot_count)
173 return
Ben Pasteneff8e1f122024-03-22 17:03:16174 if line == self.STEP_CLOSED_TEXT:
Struan Shrimptond98829f2024-03-12 19:41:38175 self._single_line_logger.log(self._current_log_level,
176 '\33[2K\rStill waiting on: 0 shard(s)...')
Struan Shrimpton3b28ff02024-04-03 22:46:05177 basic_logger.log(self._current_log_level, '')
Struan Shrimptond98829f2024-03-12 19:41:38178
179 def _ProcessResult(self, line):
180 matches = self._result_links_re.match(line)
181 if matches:
Struan Shrimpton3b28ff02024-04-03 22:46:05182 basic_logger.log(self._current_log_level,
183 'Test results for %s shard %s: %s',
184 self._current_step_name, matches[1], matches[2])
Struan Shrimptona64cb192024-03-05 02:22:00185
186 def ProcessLine(self, line):
187 # If we're in a new step see if it needs to be parsed differently
Struan Shrimptond98829f2024-03-12 19:41:38188 if line.startswith(self.SEED_STEP_TEXT):
189 self._current_step_name = line[len(self.SEED_STEP_TEXT
190 ):-len(self.ANNOTATOR_PREFIX_SUFIX)]
191 self._current_proccess_fn = self._get_processor(self._current_step_name)
192 self._current_log_level = self._get_log_level(self._current_step_name)
Struan Shrimptona64cb192024-03-05 02:22:00193 self._current_proccess_fn(line)
194 self._last_line = line
Struan Shrimpton33065b92024-11-15 23:00:15195 self._last_line_teriminal_lines = int(
196 (len(line) - 1) / self._terminal_columns) + 1
Struan Shrimpton5267e0f2024-03-28 22:22:26197 if line.startswith(self.STEP_CLOSED_TEXT):
198 # Text outside of steps will use the last processor otherwise
199 self._current_log_level = logging.DEBUG
200 _current_proccess_fn = self._StepNameProcessLine
Struan Shrimptond98829f2024-03-12 19:41:38201
202 def _get_processor(self, step_name):
203 if step_name in self._step_to_processors:
204 return self._step_to_processors[step_name]
Ben Pasteneff8e1f122024-03-22 17:03:16205 for match_name in self._step_to_processors:
206 if step_name.startswith(match_name):
207 return self._step_to_processors[match_name]
Struan Shrimptond98829f2024-03-12 19:41:38208 return self._StepNameProcessLine
209
210 def _get_log_level(self, step_name):
211 if step_name in self._step_to_log_level:
212 return self._step_to_log_level[step_name]
Ben Pasteneff8e1f122024-03-22 17:03:16213 for match_name in self._step_to_log_level:
214 if step_name.startswith(match_name):
215 return self._step_to_log_level[match_name]
Struan Shrimptond98829f2024-03-12 19:41:38216 return logging.INFO