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:' |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 42 | |
| 43 | def __init__(self): |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 44 | self._trigger_link_re = re.compile(r'.+@(https://.+)@@@$') |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 45 | self._ninja_status_re = re.compile(r'\[(\d+)\/(\d+)\]') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 46 | 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 Pastene | 37651fc50 | 2024-03-19 22:32:41 | [diff] [blame] | 52 | |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 53 | self._current_proccess_fn = self._StepNameProcessLine |
Struan Shrimpton | 5267e0f | 2024-03-28 22:22:26 | [diff] [blame] | 54 | # 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 Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 58 | self._step_to_processors = { |
Ben Pastene | ca5ad4b | 2024-03-05 19:02:47 | [diff] [blame] | 59 | 'compile': self._ProcessCompileLine, |
| 60 | 'reclient compile': self._ProcessCompileLine, |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 61 | 'test_pre_run.[trigger] ': self._ProcessTriggerLine, |
| 62 | 'collect tasks.wait for tasks': self._ProcessCollectLine, |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 63 | } |
Struan Shrimpton | 5267e0f | 2024-03-28 22:22:26 | [diff] [blame] | 64 | # 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 Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 67 | self._step_to_log_level = { |
Struan Shrimpton | 5267e0f | 2024-03-28 22:22:26 | [diff] [blame] | 68 | '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 Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 77 | '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 Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 93 | } |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 94 | # 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 Pastene | 37651fc50 | 2024-03-19 22:32:41 | [diff] [blame] | 100 | |
| 101 | self._last_line = '' |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame^] | 102 | self._last_line_teriminal_lines = 0 |
Ben Pastene | 37651fc50 | 2024-03-19 22:32:41 | [diff] [blame] | 103 | self._current_log_level = logging.DEBUG |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 104 | self._single_line_logger = logger |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame^] | 105 | self._terminal_columns, _ = os.get_terminal_size() |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 106 | self._current_step_name = '' |
| 107 | self._dot_count = 0 |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 108 | |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 109 | def _StdoutProcessLine(self, line): |
| 110 | if not line.startswith(self.ANNOTATOR_PREFIX_SUFIX): |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 111 | # Pass through any non-engine text |
Ben Pastene | 2446d70a | 2024-07-15 21:44:16 | [diff] [blame] | 112 | 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 Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 117 | |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 118 | 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 Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 122 | '\n[cyan]Running: ' + self._current_step_name + '[/]') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 123 | return |
Ben Pastene | 2446d70a | 2024-07-15 21:44:16 | [diff] [blame] | 124 | self._StdoutProcessLine(line) |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 125 | |
| 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 Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 139 | basic_logger.log(self._current_log_level, |
| 140 | f'Triggered {task_name}: ' + matches[1]) |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 141 | else: |
| 142 | self._StdoutProcessLine(line) |
| 143 | |
Ben Pastene | ca5ad4b | 2024-03-05 19:02:47 | [diff] [blame] | 144 | def _ProcessCompileLine(self, line): |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 145 | if line.startswith(self.SEED_STEP_TEXT): |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 146 | logging.info('\n[cyan]Running: ' + self._current_step_name + '[/]') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 147 | return |
Struan Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 148 | matches = self._ninja_status_re.match(line) |
| 149 | if matches: |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame^] | 150 | # 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 Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 157 | return |
Ben Pastene | ff8e1f12 | 2024-03-22 17:03:16 | [diff] [blame] | 158 | if self._last_line.startswith('['): |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 159 | basic_logger.log(self._current_log_level, '') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 160 | self._StdoutProcessLine(line) |
| 161 | |
| 162 | def _ProcessCollectLine(self, line): |
| 163 | if line.startswith(self.SEED_STEP_TEXT): |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 164 | logging.info('\n[cyan]Running: ' + self._current_step_name + '[/]') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 165 | 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 Pastene | ff8e1f12 | 2024-03-22 17:03:16 | [diff] [blame] | 174 | if line == self.STEP_CLOSED_TEXT: |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 175 | self._single_line_logger.log(self._current_log_level, |
| 176 | '\33[2K\rStill waiting on: 0 shard(s)...') |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 177 | basic_logger.log(self._current_log_level, '') |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 178 | |
| 179 | def _ProcessResult(self, line): |
| 180 | matches = self._result_links_re.match(line) |
| 181 | if matches: |
Struan Shrimpton | 3b28ff0 | 2024-04-03 22:46:05 | [diff] [blame] | 182 | 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 Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 185 | |
| 186 | def ProcessLine(self, line): |
| 187 | # 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] | 188 | 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 Shrimpton | a64cb19 | 2024-03-05 02:22:00 | [diff] [blame] | 193 | self._current_proccess_fn(line) |
| 194 | self._last_line = line |
Struan Shrimpton | 33065b9 | 2024-11-15 23:00:15 | [diff] [blame^] | 195 | self._last_line_teriminal_lines = int( |
| 196 | (len(line) - 1) / self._terminal_columns) + 1 |
Struan Shrimpton | 5267e0f | 2024-03-28 22:22:26 | [diff] [blame] | 197 | 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 Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 201 | |
| 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 Pastene | ff8e1f12 | 2024-03-22 17:03:16 | [diff] [blame] | 205 | for match_name in self._step_to_processors: |
| 206 | if step_name.startswith(match_name): |
| 207 | return self._step_to_processors[match_name] |
Struan Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 208 | 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 Pastene | ff8e1f12 | 2024-03-22 17:03:16 | [diff] [blame] | 213 | 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 Shrimpton | d98829f | 2024-03-12 19:41:38 | [diff] [blame] | 216 | return logging.INFO |