| #!/usr/bin/env python3 | |
| # Copyright 2026 The PDFium Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Finds surprise passes (unexpected successes) in ResultDB for a Gerrit CL.""" | |
| import argparse | |
| import collections | |
| import json | |
| import subprocess | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| from githelper import GitHelper | |
| def GetLatestPatchset(issue): | |
| """Returns the highest patchset number for a Gerrit issue.""" | |
| url = f'https://pdfium-review.googlesource.com/changes/{issue}' | |
| try: | |
| with urllib.request.urlopen(url, timeout=10) as response: | |
| raw = response.read().decode('utf-8') | |
| data = json.loads(raw.removeprefix(")]}'\n")) | |
| return data.get('current_revision_number') | |
| except (urllib.error.URLError, json.JSONDecodeError, ValueError, OSError): | |
| return None | |
| def GetTryResults(issue, patchset=None): | |
| """Fetches tryjob results as JSON via git cl try-results.""" | |
| if patchset: | |
| patchsets = [patchset] | |
| else: | |
| latest = GetLatestPatchset(issue) | |
| patchsets = range(latest, 0, -1) if latest else [None] | |
| for ps in patchsets: | |
| cmd = ['git', 'cl', 'try-results', '--json=-', '--issue', str(issue)] | |
| if ps is not None: | |
| cmd.extend(['--patchset', str(ps)]) | |
| result = subprocess.run(cmd, capture_output=True, text=True, check=False) | |
| try: | |
| builds = json.loads(result.stdout) | |
| if builds: | |
| return builds | |
| except (json.JSONDecodeError, ValueError): | |
| pass | |
| return [] | |
| def QueryResultDB(invocation_ids, batch_size=50): | |
| """Queries ResultDB for test results across invocations.""" | |
| for i in range(0, len(invocation_ids), batch_size): | |
| batch = invocation_ids[i:i + batch_size] | |
| cmd = ['rdb', 'query', '-json', '-tr-fields', 'testId,status,tags,variant' | |
| ] + batch | |
| proc = subprocess.Popen( | |
| cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) | |
| for line in proc.stdout: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| try: | |
| data = json.loads(line) | |
| tr = data.get('testResult') | |
| if tr: | |
| yield tr | |
| except json.JSONDecodeError: | |
| pass | |
| proc.wait() | |
| def ExtractTags(test_result): | |
| """Extracts tags list into a dictionary of key -> value.""" | |
| tags = {} | |
| for item in test_result.get('tags', []): | |
| key = item.get('key') | |
| value = item.get('value') | |
| if key: | |
| tags[key] = value | |
| return tags | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Query ResultDB for surprise passes in a Gerrit issue.') | |
| parser.add_argument( | |
| '--issue', | |
| '-i', | |
| help='Gerrit issue number (defaults to current branch issue)') | |
| parser.add_argument( | |
| '--patchset', | |
| '-p', | |
| help='Patchset number (defaults to latest patchset with tryjobs)') | |
| parser.add_argument( | |
| '--all-skips', | |
| action='store_true', | |
| help='Also include ordinary skipped tests in output') | |
| parser.add_argument( | |
| '--by-builder', | |
| action='store_true', | |
| help='Group results by builder instead of by test') | |
| parser.add_argument( | |
| '--json', action='store_true', help='Output raw results as JSON') | |
| args = parser.parse_args() | |
| git = GitHelper() | |
| branch = git.GetCurrentBranchName() | |
| issue = args.issue or git.GetGerritIssue(branch) | |
| if not issue: | |
| print( | |
| 'Error: Could not determine Gerrit issue for current branch. ' | |
| 'Specify --issue manually.', | |
| file=sys.stderr) | |
| return 1 | |
| if not args.json: | |
| print(f'Branch: {branch or "(detached HEAD)"}') | |
| print(f'Gerrit Issue: {issue}') | |
| tryjobs = GetTryResults(issue, patchset=args.patchset) | |
| if not tryjobs: | |
| if args.json: | |
| print('{}') | |
| else: | |
| print('No tryjob results found.') | |
| return 0 | |
| # Filter out non-test builders that lack test invocations (e.g. presubmit). | |
| test_builds = [ | |
| b for b in tryjobs | |
| if 'id' in b and b.get('builder', {}).get('builder') != 'pdfium_presubmit' | |
| ] | |
| invocation_ids = [f'build-{b["id"]}' for b in test_builds] | |
| if not args.json: | |
| print(f'Querying ResultDB across {len(invocation_ids)} tryjob builds...') | |
| surprises = [] | |
| ordinary_skips = [] | |
| for tr in QueryResultDB(invocation_ids): | |
| status = tr.get('status') | |
| if status != 'SKIP': | |
| continue | |
| tags = ExtractTags(tr) | |
| builder = tr.get('variant', {}).get('def', {}).get('builder', 'unknown') | |
| test_id = tr.get('testId', 'unknown') | |
| suppression_tag = tags.get('pdfium_suppression') | |
| entry = { | |
| 'test_id': test_id, | |
| 'builder': builder, | |
| 'status': status, | |
| 'pdfium_suppression': suppression_tag or '(none)', | |
| 'is_surprise': suppression_tag == 'unexpected_success', | |
| 'raw_status': tags.get('raw_status', '(none)'), | |
| 'step_name': tags.get('step_name', '(none)') | |
| } | |
| if entry['is_surprise']: | |
| surprises.append(entry) | |
| else: | |
| ordinary_skips.append(entry) | |
| if args.json: | |
| out = {'surprises': surprises} | |
| if args.all_skips: | |
| out['ordinary_skips'] = ordinary_skips | |
| print(json.dumps(out, indent=2)) | |
| return 0 | |
| print('\n=== Surprise Passes (Unexpected Successes) ===') | |
| if not surprises: | |
| print('No surprise passes found.') | |
| elif args.by_builder: | |
| builder_map = collections.defaultdict(list) | |
| for s in surprises: | |
| builder_map[s['builder']].append(s) | |
| for builder in sorted(builder_map.keys()): | |
| entries = builder_map[builder] | |
| print(f'\nBuilder: {builder} ({len(entries)} entries)') | |
| for e in sorted(entries, key=lambda x: (x['test_id'], x['step_name'])): | |
| print(f' {e["test_id"]}') | |
| print(f' status: {e["status"]}') | |
| print(f' tag: pdfium_suppression={e["pdfium_suppression"]}') | |
| if e['step_name'] != '(none)': | |
| print(f' step: {e["step_name"]}') | |
| else: | |
| test_map = collections.defaultdict(list) | |
| for s in surprises: | |
| test_map[s['test_id']].append(s) | |
| for test_id in sorted(test_map.keys()): | |
| entries = test_map[test_id] | |
| tag_val = entries[0]['pdfium_suppression'] | |
| status = entries[0]['status'] | |
| print(f'\nTest: {test_id}') | |
| print(f' status: {status}') | |
| print(f' tag: pdfium_suppression={tag_val}') | |
| steps = sorted( | |
| set(f'[{e["builder"]}] {e["step_name"]}' if e['step_name'] != | |
| '(none)' else f'[{e["builder"]}]' for e in entries)) | |
| print(f' configurations ({len(entries)}):') | |
| for step in steps: | |
| print(f' - {step}') | |
| print('\n----------------------------------------') | |
| distinct_tests = len(set(s['test_id'] for s in surprises)) | |
| distinct_builders = len(set(s['builder'] for s in surprises)) | |
| print( | |
| f'Total surprise passes: {len(surprises)} occurrences ' | |
| f'across {distinct_tests} distinct tests on {distinct_builders} builders.' | |
| ) | |
| if args.all_skips: | |
| print('\n=== Ordinary Skips (No Surprise Tag) ===') | |
| distinct_ord = len(set(s['test_id'] for s in ordinary_skips)) | |
| print(f'Total ordinary skips: {len(ordinary_skips)} occurrences ' | |
| f'across {distinct_ord} distinct tests.') | |
| return 0 | |
| if __name__ == '__main__': | |
| sys.exit(main()) |