Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 1 | # 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 Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 4 | """Adapter for printing legacy recipe output |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 5 | |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 6 | TODO(https://crbug.com/326904531): This file is intended to be a temporary |
| 7 | workaround and should be replaced once this bug is resolved""" |
| 8 | |
| 9 | import json |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 10 | import logging |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame] | 11 | import os |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 12 | import re |
| 13 | import sys |
| 14 | |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 15 | # Create a logger that avoids rich formatting as we can't control recipe |
| 16 | # formatting from here |
| 17 | basic_logger = logging.getLogger('basic_logger') |
| 18 | basic_logger.addHandler(logging.StreamHandler(sys.stdout)) |
| 19 | basic_logger.propagate = False |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 20 | |
Ben Pastene | ca5ad4b | 2024-03-05 19:02:47 | [diff] [blame] | 21 | class PassthroughAdapter: |
| 22 | """Doesn't filter anything, just logs everything from the recipe run.""" |
| 23 | |
| 24 | def ProcessLine(self, line): |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 25 | basic_logger.log(logging.DEBUG, line) |
Ben Pastene | ca5ad4b | 2024-03-05 19:02:47 | [diff] [blame] | 26 | |
| 27 | |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 28 | class LegacyOutputAdapter: |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 29 | """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:' |
Ben Pastene | 30f4b3c | 2024-11-22 22:57:50 | [diff] [blame] | 42 | # Special sub-log names added by the UTR recipe to surface to users. |
| 43 | UTR_LOG_NAME = 'utr_log' |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 44 | |
| 45 | def __init__(self): |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 46 | self._trigger_link_re = re.compile(r'.+@(https://.+)@@@$') |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 47 | self._ninja_status_re = re.compile(r'\[(\d+)\/(\d+)\]') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 48 | self._collect_wait_re = re.compile( |
| 49 | r'.+prpc call (.+) swarming.v2.Tasks.ListTaskStates, stdin: ' |
| 50 | r'(\{"task_id": .+\})$' |
| 51 | ) |
| 52 | self._result_links_re = re.compile( |
| 53 | r'@@@STEP_LINK@shard (#\d+) test results@(https://[^@]+)@@@') |
Ben Pastene | 37651fc50 | 2024-03-19 22:32:41 | [diff] [blame] | 54 | |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 55 | self._current_proccess_fn = self._StepNameProcessLine |
Struan Shrimpton | 5267e0f | 2024-03-28 22:22:26 | [diff] [blame] | 56 | # The first match is used. This allows us to filter parent steps while still |
| 57 | # printing child steps by adding the child step name first. By default |
| 58 | # _StepNameProcessLine will be used which prints the step name and it's |
| 59 | # stdout |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 60 | self._step_to_processors = { |
Ben Pastene | ca5ad4b | 2024-03-05 19:02:47 | [diff] [blame] | 61 | 'compile': self._ProcessCompileLine, |
| 62 | 'reclient compile': self._ProcessCompileLine, |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 63 | 'test_pre_run.[trigger] ': self._ProcessTriggerLine, |
| 64 | 'collect tasks.wait for tasks': self._ProcessCollectLine, |
Ben Pastene | 7057098 | 2024-11-19 22:08:47 | [diff] [blame] | 65 | 'download compilation outputs': self._PrintOnlyStepName, |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 66 | } |
Struan Shrimpton | 5267e0f | 2024-03-28 22:22:26 | [diff] [blame] | 67 | # The first match is used. This allows us to filter parent steps while still |
| 68 | # printing child steps by adding the child step name first. By default INFO |
| 69 | # will be used which prints in non-verbose mode (i.e. no -v flag) |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 70 | self._step_to_log_level = { |
Struan Shrimpton | 5267e0f | 2024-03-28 22:22:26 | [diff] [blame] | 71 | 'lookup_builder_gn_args': logging.DEBUG, |
| 72 | 'git rev-parse': logging.DEBUG, |
| 73 | 'git diff to instrument': logging.DEBUG, |
| 74 | 'save paths of affected files': logging.DEBUG, |
| 75 | 'preprocess for reclient.start reproxy via bootstrap': logging.INFO, |
| 76 | 'preprocess for reclient': logging.DEBUG, |
| 77 | 'process clang crashes': logging.DEBUG, |
| 78 | 'compile confirm no-op': logging.DEBUG, |
| 79 | 'postprocess for reclient': logging.DEBUG, |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 80 | 'setup_build': logging.DEBUG, |
| 81 | 'get compile targets for scripts': logging.DEBUG, |
| 82 | 'lookup GN args': logging.DEBUG, |
| 83 | 'install infra/tools/luci/isolate': logging.DEBUG, |
| 84 | 'find command lines': logging.DEBUG, |
| 85 | 'test_pre_run.install infra/tools/luci/swarming': logging.DEBUG, |
| 86 | 'isolate tests': logging.DEBUG, |
| 87 | 'read GN args': logging.DEBUG, |
| 88 | 'test_pre_run.[trigger] ': logging.INFO, |
| 89 | 'test_pre_run.': logging.DEBUG, |
| 90 | 'collect tasks.wait for tasks': logging.INFO, |
| 91 | 'collect tasks': logging.DEBUG, |
| 92 | '$debug - all results': logging.DEBUG, |
| 93 | 'Test statistics': logging.DEBUG, |
| 94 | 'read gclient': logging.DEBUG, |
| 95 | 'write output_properties_file': logging.DEBUG, |
Struan Shrimpton | ca55bdad | 2025-01-28 21:16:49 | [diff] [blame] | 96 | 'prepare skylab tests.': logging.DEBUG, |
Struan Shrimpton | 15aa41e | 2025-05-07 21:51:17 | [diff] [blame] | 97 | 'update invocation instructions': logging.DEBUG, |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 98 | } |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 99 | # Setup logger for printing to the same line |
| 100 | logger = logging.getLogger('single_line_logger') |
| 101 | handler = logging.StreamHandler(sys.stdout) |
| 102 | handler.terminator = '' |
| 103 | logger.addHandler(handler) |
| 104 | logger.propagate = False |
Ben Pastene | 37651fc50 | 2024-03-19 22:32:41 | [diff] [blame] | 105 | |
| 106 | self._last_line = '' |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame] | 107 | self._last_line_teriminal_lines = 0 |
Ben Pastene | 37651fc50 | 2024-03-19 22:32:41 | [diff] [blame] | 108 | self._current_log_level = logging.DEBUG |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 109 | self._single_line_logger = logger |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame] | 110 | self._terminal_columns, _ = os.get_terminal_size() |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 111 | self._current_step_name = '' |
| 112 | self._dot_count = 0 |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 113 | |
Ben Pastene | 7057098 | 2024-11-19 22:08:47 | [diff] [blame] | 114 | def _PrintCurrentStepName(self, log_level): |
| 115 | logging.log(log_level, '\n[cyan]Running: %s[/]', self._current_step_name) |
| 116 | |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 117 | def _StdoutProcessLine(self, line): |
Ben Pastene | 30f4b3c | 2024-11-22 22:57:50 | [diff] [blame] | 118 | # Pass through any non-engine or utr-log text. |
| 119 | if line.startswith(f'@@@STEP_LOG_LINE@{self.UTR_LOG_NAME}@'): |
| 120 | # '-3' corresponds to the trailing @@@ on every sub-log line. |
| 121 | line = line[len(f'@@@STEP_LOG_LINE@{self.UTR_LOG_NAME}@'):-3] |
| 122 | if line.startswith(self.ANNOTATOR_PREFIX_SUFIX): |
| 123 | return |
| 124 | is_urlish = re.match(r'^http[s]?://\S+$', line) |
| 125 | if is_urlish: |
| 126 | logging.log(self._current_log_level, line) |
| 127 | else: |
| 128 | basic_logger.log(self._current_log_level, line) |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 129 | |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 130 | def _StepNameProcessLine(self, line): |
| 131 | if line.startswith(self.SEED_STEP_TEXT): |
| 132 | # Always print the step name to info |
Ben Pastene | 7057098 | 2024-11-19 22:08:47 | [diff] [blame] | 133 | self._PrintCurrentStepName(self._current_log_level) |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 134 | return |
Ben Pastene | 2446d70a | 2024-07-15 21:44:16 | [diff] [blame] | 135 | self._StdoutProcessLine(line) |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 136 | |
Ben Pastene | 7057098 | 2024-11-19 22:08:47 | [diff] [blame] | 137 | def _PrintOnlyStepName(self, line): |
| 138 | if line.startswith(self.SEED_STEP_TEXT): |
| 139 | self._PrintCurrentStepName(logging.INFO) |
| 140 | |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 141 | def _ProcessTriggerLine(self, line): |
| 142 | if line.startswith(self.SEED_STEP_TEXT + self.TRIGGER_STEP_PREFIX): |
| 143 | # The step names for tests don't have any identifying keywords so the |
| 144 | # result step parsers need to be installed at trigger time |
| 145 | test_name = line[len(self.SEED_STEP_TEXT + |
| 146 | self.TRIGGER_STEP_PREFIX):line.index(' (') if ' (' in |
| 147 | line else -len(self.ANNOTATOR_PREFIX_SUFIX)] |
| 148 | self._step_to_processors[test_name] = self._ProcessResult |
| 149 | self._step_to_log_level[test_name] = logging.DEBUG |
| 150 | elif line.startswith(self.TRIGGER_LINK_TEXT): |
| 151 | matches = self._trigger_link_re.match(line) |
| 152 | if matches: |
| 153 | task_name = self._current_step_name[len(self.TRIGGER_STEP_PREFIX):] |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 154 | basic_logger.log(self._current_log_level, |
| 155 | f'Triggered {task_name}: ' + matches[1]) |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 156 | else: |
| 157 | self._StdoutProcessLine(line) |
| 158 | |
Ben Pastene | ca5ad4b | 2024-03-05 19:02:47 | [diff] [blame] | 159 | def _ProcessCompileLine(self, line): |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 160 | if line.startswith(self.SEED_STEP_TEXT): |
Ben Pastene | 7057098 | 2024-11-19 22:08:47 | [diff] [blame] | 161 | self._PrintCurrentStepName(logging.INFO) |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 162 | return |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 163 | matches = self._ninja_status_re.match(line) |
| 164 | if matches: |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame] | 165 | # Remove the last line which might be multiple on the terminal |
| 166 | self._single_line_logger.log(self._current_log_level, '\33[2K') |
| 167 | if self._last_line_teriminal_lines > 1: |
| 168 | for _ in range(self._last_line_teriminal_lines - 1): |
| 169 | self._single_line_logger.log(self._current_log_level, '\33[A\33[2K') |
| 170 | self._single_line_logger.log(self._current_log_level, '\r' + line) |
| 171 | self._single_line_logger.handlers[0].flush() |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 172 | return |
Ben Pastene | ff8e1f12 | 2024-03-22 17:03:16 | [diff] [blame] | 173 | if self._last_line.startswith('['): |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 174 | basic_logger.log(self._current_log_level, '') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 175 | self._StdoutProcessLine(line) |
| 176 | |
| 177 | def _ProcessCollectLine(self, line): |
| 178 | if line.startswith(self.SEED_STEP_TEXT): |
Ben Pastene | 7057098 | 2024-11-19 22:08:47 | [diff] [blame] | 179 | self._PrintCurrentStepName(logging.INFO) |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 180 | matches = self._collect_wait_re.match(line) |
| 181 | if matches: |
| 182 | task_ids = json.loads(matches[2])['task_id'] |
| 183 | self._dot_count = (self._dot_count % 5) + 1 |
| 184 | self._single_line_logger.log( |
| 185 | self._current_log_level, |
| 186 | f'\33[2K\rStill waiting on: {len(task_ids)} shard(s)' + |
| 187 | '.' * self._dot_count) |
| 188 | return |
Ben Pastene | ff8e1f12 | 2024-03-22 17:03:16 | [diff] [blame] | 189 | if line == self.STEP_CLOSED_TEXT: |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 190 | self._single_line_logger.log(self._current_log_level, |
| 191 | '\33[2K\rStill waiting on: 0 shard(s)...') |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 192 | basic_logger.log(self._current_log_level, '') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 193 | |
| 194 | def _ProcessResult(self, line): |
| 195 | matches = self._result_links_re.match(line) |
| 196 | if matches: |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 197 | basic_logger.log(self._current_log_level, |
| 198 | 'Test results for %s shard %s: %s', |
| 199 | self._current_step_name, matches[1], matches[2]) |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 200 | |
| 201 | def ProcessLine(self, line): |
| 202 | # If we're in a new step see if it needs to be parsed differently |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 203 | if line.startswith(self.SEED_STEP_TEXT): |
| 204 | self._current_step_name = line[len(self.SEED_STEP_TEXT |
| 205 | ):-len(self.ANNOTATOR_PREFIX_SUFIX)] |
| 206 | self._current_proccess_fn = self._get_processor(self._current_step_name) |
| 207 | self._current_log_level = self._get_log_level(self._current_step_name) |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 208 | self._current_proccess_fn(line) |
| 209 | self._last_line = line |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame] | 210 | self._last_line_teriminal_lines = int( |
| 211 | (len(line) - 1) / self._terminal_columns) + 1 |
Struan Shrimpton | 5267e0f | 2024-03-28 22:22:26 | [diff] [blame] | 212 | if line.startswith(self.STEP_CLOSED_TEXT): |
| 213 | # Text outside of steps will use the last processor otherwise |
| 214 | self._current_log_level = logging.DEBUG |
| 215 | _current_proccess_fn = self._StepNameProcessLine |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 216 | |
| 217 | def _get_processor(self, step_name): |
| 218 | if step_name in self._step_to_processors: |
| 219 | return self._step_to_processors[step_name] |
Ben Pastene | ff8e1f12 | 2024-03-22 17:03:16 | [diff] [blame] | 220 | for match_name in self._step_to_processors: |
| 221 | if step_name.startswith(match_name): |
| 222 | return self._step_to_processors[match_name] |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 223 | return self._StepNameProcessLine |
| 224 | |
| 225 | def _get_log_level(self, step_name): |
| 226 | if step_name in self._step_to_log_level: |
| 227 | return self._step_to_log_level[step_name] |
Ben Pastene | ff8e1f12 | 2024-03-22 17:03:16 | [diff] [blame] | 228 | for match_name in self._step_to_log_level: |
| 229 | if step_name.startswith(match_name): |
| 230 | return self._step_to_log_level[match_name] |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 231 | return logging.INFO |