Skip to content

Commit 40c1bc7

Browse files
Liviu RauDevtools-frontend LUCI CQ
authored andcommitted
Collect goldens from resultdb
- remove deprecated v2 script - updated reporter to collect path info - small structural refactoring and some renames Tested on the CL on top. Bug: 408113752 Change-Id: I16c34b3c023e5d83fab37c75034da31630c4eeb3 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6552018 Reviewed-by: Alex Rudenko <alexrudenko@chromium.org> Commit-Queue: Liviu Rau <liviurau@chromium.org>
1 parent 513ab8b commit 40c1bc7

File tree

10 files changed

+190
-358
lines changed

10 files changed

+190
-358
lines changed

front_end/panels/timeline/docs/flame_chart_migration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ Migrating a track consists of taking the code in the data provider corresponding
2525

2626
After adding the test file, you can run `npm run auto-screenshotstest` to generate the screenshot locally to check before submitting.
2727

28-
Or you can upload to the Gerrit and after the screenshot tests fails, run `./scripts/tools/update_goldens_v2.py` to update the screenshots.
29-
See [update_goldens_v2.py](../../../scripts/tools/update_goldens_v2.py) for more information.
28+
Or you can upload to the Gerrit and after the screenshot tests fails, run `./scripts/tools/update_goldens.py` to update the screenshots.
29+
See [update_goldens.py](../../../scripts/tools/update_goldens.py) for more information.
3030

3131
1. Add missing related functionality to the new engine (not always needed).
3232

scripts/tools/update_goldens.py

Lines changed: 142 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,40 @@
22
# Copyright 2023 The DevTools Authors. All rights reserved.
33
# Use of this source code is governed by a BSD-style license that can be
44
# found in the LICENSE file.
5-
"""This tool aims to assist you with screenshot changes.
6-
7-
Whenever your changes impact the screenshots in the Interaction tests you will
8-
need to update (or add) those screenshots for all the supported platforms.
9-
10-
Assuming that you committed your current changes and uploaded the CL, you will
11-
first need to trigger a group of special builders that will try to detect any
12-
such screenshot changes. Use:
13-
\x1b[32m update_goldens.py trigger \x1b[0m
14-
for that.
15-
16-
After you wait for those builders to finish you will want to get the proposed
17-
changes on your development environment. Simply run:
18-
\x1b[32m update_goldens.py update \x1b[0m
19-
If there are still builders that did not finish you will be notified and asked
20-
to wait a little longer.
21-
22-
Finally inspect the screenshot changes that are now present (if any) on your
23-
development machine. If they look as expected add, commit and upload. You
24-
should not get any additional screenshot changes if you repeat the steps above,
25-
provided you did not perform any additional changes in the code.
5+
"""The purpose of this CLI tool is to help you manage changes to screenshots in
6+
tests across multiple platforms.
7+
8+
For more information, see test/interactions/README.md.
9+
10+
If you've made changes that impact the screenshots, you'll need to update them
11+
for all supported platforms. Assuming you've committed your changes and
12+
uploaded the CL, you'll need to trigger a dry run in Gerrit or execute the
13+
command:
14+
\x1b[32m git cl try \x1b[0m
15+
16+
After waiting for the dry run to complete, you can execute the command:
17+
\x1b[32m update_goldens.py \x1b[0m
18+
19+
Any failing test will generate updated screenshots, which will be downloaded and
20+
applied to your local change. Inspect the status of your working copy for any
21+
such screenshot updates. If you have new screenshots and they look as expected,
22+
add, commit, and upload the changes. If you repeat the steps above without
23+
making any additional changes to the code, you should not have any more
24+
screenshot tests failing.
2625
"""
2726

2827
import argparse
2928
import json
3029
import os
3130
import re
32-
import tempfile
3331
import time
32+
import shutil
33+
import ssl
3434
import subprocess
3535
import sys
36+
import urllib.request
3637

38+
ssl._create_default_https_context = ssl._create_unverified_context
3739

3840
class ProjectConfig:
3941
def __init__(self,
@@ -81,84 +83,142 @@ def __init__(self,
8183
'applied to your local repo.\n To quickly see what is new just run "git ' \
8284
'status".'
8385

84-
8586
def main(project_config, *args):
8687
parser = build_parser()
8788
options = parser.parse_args(*args)
88-
if not options.command:
89-
parser.print_help()
90-
sys.exit(1)
91-
options.func(project_config, options)
89+
update(project_config, options)
9290

9391

9492
def build_parser():
9593
"""Parse command line arguments."""
9694
parser = argparse.ArgumentParser(
9795
formatter_class=argparse.RawTextHelpFormatter, epilog=__doc__)
98-
sp = parser.add_subparsers(dest='command')
99-
100-
trigger_help = 'Triggers screenshot builders for the current patchset.'
101-
trigger_parser = sp.add_parser('trigger',
102-
description=trigger_help,
103-
help=trigger_help)
104-
trigger_parser.add_argument(
105-
'--ignore-triggered',
106-
action='store_true',
107-
help='Ignore any existing results or triggered builders on the '
108-
'current patch.')
109-
trigger_parser.set_defaults(func=trigger)
110-
111-
update_help = 'Downloads the screenshots from the builders and applies ' \
112-
'them locally.'
113-
update_parser = sp.add_parser('update',
114-
description=update_help,
115-
help=update_help)
116-
mutually_exclusive = update_parser.add_mutually_exclusive_group()
117-
mutually_exclusive.add_argument(
118-
'--patchset',
119-
help='The patchset number from where to download screenshot changes. '
120-
'If not provided it defaults to the latest patchset.')
121-
update_parser.add_argument(
122-
'--ignore-failed',
123-
action='store_true',
124-
help='Ignore results comming from failed builders.')
125-
mutually_exclusive.add_argument(
126-
'--retry',
127-
action='store_true',
128-
help='Re-trigger failed builders (when dealing with flakes).')
129-
update_parser.add_argument('--wait-sec', type=int,
96+
97+
parser.add_argument('--wait-sec', type=int,
13098
help='Wait and retry update every specified number of seconds. ' \
13199
'Minimum value is 30s to avoid overwhelming Gerrit.')
132-
update_parser.set_defaults(func=update)
133-
134-
help_help = 'Show help.'
135-
help_parser = sp.add_parser('help', description=help_help, help=help_help)
136-
help_parser.add_argument('name',
137-
nargs='?',
138-
help='Command to show help for')
139-
help_parser.set_defaults(func=get_help(parser, sp))
140-
100+
parser.set_defaults(func=update)
141101
parser.add_argument('--verbose',
142102
action='store_true',
143103
help='Show more debugging info')
144104

145-
return parser
146-
147-
148-
def trigger(project_config, options):
149-
check_results_exist(project_config, options.ignore_triggered)
150-
trigger_screenshots(project_config, options.verbose)
105+
#Deprecated options. These are no longer used, but are kept here
106+
#to avoid breaking existing scripts."""
107+
parser.add_argument('--patchset',
108+
help='Deprecated. Not used by this tool.')
109+
parser.add_argument('--ignore-failed',
110+
help='Deprecated. Not used by this tool.')
111+
parser.add_argument('--retry', help='Deprecated. Not used by this tool.')
151112

113+
return parser
152114

153115
def update(project_config, options):
154116
test_clean_git()
155-
test_gcs_connectivity(project_config)
156117
wait_sec = options.wait_sec
157118
if wait_sec:
158119
wait_sec = max(wait_sec, 30)
159-
apply_patch_to_local(project_config, options.patchset, wait_sec,
160-
options.ignore_failed, options.retry, options.verbose)
120+
query_rdb_for_screenshots(project_config, options.patchset, wait_sec,
121+
options.ignore_failed, options.retry,
122+
options.verbose)
123+
124+
125+
def query_rdb_for_screenshots(project_config, patchset, wait_sec,
126+
ignore_failed, retry, verbose):
127+
"""Download and apply the patches from the builders."""
128+
results = screenshot_results(project_config, patchset)
129+
check_not_empty(results)
130+
check_all_platforms(project_config, results)
131+
retry, should_wait = check_all_success(project_config, results, wait_sec,
132+
ignore_failed, retry)
133+
if retry:
134+
# Avoiding to force the user to run a 'trigger' command
135+
trigger_screenshots(project_config, retry)
136+
sys.exit(0)
137+
if not should_wait:
138+
download_generated_imgs(results)
139+
print(INFO_PATCHES_APPLIED)
140+
sys.exit(0)
141+
print(f'Waiting {wait_sec} seconds ...')
142+
time.sleep(wait_sec)
143+
query_rdb_for_screenshots(project_config, patchset, wait_sec,
144+
ignore_failed, retry, verbose)
145+
146+
147+
def download_generated_imgs(try_results):
148+
for _, try_result in try_results.items():
149+
if try_result['status'] == 'FAILURE':
150+
unexpected_results = get_unexpected_results(try_result['id'])
151+
for result in unexpected_results['testResults']:
152+
result = get_result_with_tags(result['name'])
153+
screenshot_path = get_screenshot_path(result)
154+
if not screenshot_path:
155+
continue
156+
artifacts = list_artifacts(result['name'])
157+
for artifact in artifacts['artifacts']:
158+
if artifact['artifactId'] in ['actual_image', 'generated']:
159+
download_individual_screenshot(screenshot_path,
160+
artifact['fetchUrl'])
161+
162+
163+
def get_unexpected_results(invocation_suffix):
164+
invocation_id = f'build-{invocation_suffix}'
165+
return _rdb_rpc(
166+
'QueryTestResults', {
167+
"invocations": [f'invocations/{invocation_id}'],
168+
"predicate": {
169+
"excludeExonerated": True,
170+
"expectancy": "VARIANTS_WITH_ONLY_UNEXPECTED_RESULTS",
171+
}
172+
})
173+
174+
175+
def get_result_with_tags(name):
176+
return _rdb_rpc('GetTestResult', {"name": name})
177+
178+
179+
def list_artifacts(name):
180+
return _rdb_rpc('ListArtifacts', {
181+
"parent": name,
182+
})
183+
184+
185+
def download_individual_screenshot(screenshot_path, fetchUrl):
186+
with urllib.request.urlopen(fetchUrl) as response:
187+
os.makedirs(os.path.dirname(screenshot_path), exist_ok=True)
188+
with open(screenshot_path, 'w+b') as screenshot_file:
189+
shutil.copyfileobj(response, screenshot_file)
190+
191+
192+
def _rdb_rpc(service, request_payload):
193+
results_command = ['rdb', 'rpc', 'luci.resultdb.v1.ResultDB']
194+
results_command.append(service)
195+
p = subprocess.Popen(results_command,
196+
stdin=subprocess.PIPE,
197+
stdout=subprocess.PIPE,
198+
stderr=subprocess.PIPE,
199+
text=True)
200+
201+
stdout, stderr = p.communicate(json.dumps(request_payload))
202+
if p.returncode != 0:
203+
# rdb doesn't return unique status codes for different errors, so we have to
204+
# just match on the output.
205+
if 'interactive login is required' in stderr:
206+
print("Authentication is required to fetch test metadata.\n" +
207+
"Please run:\n\trdb auth-login\nand try again")
208+
else:
209+
print(f'rdb rpc {service} failed with: {stderr}')
210+
sys.exit(1)
161211

212+
return json.loads(stdout)
213+
214+
215+
def get_screenshot_path(individual_result):
216+
for tag in individual_result['tags']:
217+
if tag['key'] == 'run_phase' and tag['value'] != 'default':
218+
return None
219+
if tag['key'] == 'screenshot_path':
220+
return tag['value'].replace('\\', '/')
221+
return None
162222

163223
def get_help(parser, subparsers):
164224
def _help(options):
@@ -203,43 +263,6 @@ def test_clean_git():
203263
sys.exit(0)
204264

205265

206-
def test_gcs_connectivity(project_config):
207-
"""Test if gcloud needs to be configured for current user."""
208-
process = subprocess.Popen(gcstorage_cmd('ls', project_config.gs_root),
209-
stdout=subprocess.PIPE,
210-
stderr=subprocess.PIPE)
211-
_, stderr = process.communicate()
212-
if process.returncode != 0:
213-
print(stderr.decode('utf-8'), '\n')
214-
print(WARNING_GCS_CONNECTIVITY)
215-
sys.exit(0)
216-
217-
218-
def apply_patch_to_local(project_config, patchset, wait_sec, ignore_failed,
219-
retry, verbose):
220-
"""Download and apply the patches from the builders."""
221-
results = screenshot_results(project_config, patchset)
222-
check_not_empty(results)
223-
check_all_platforms(project_config, results)
224-
retry, should_wait = check_all_success(project_config, results, wait_sec,
225-
ignore_failed, retry)
226-
if retry:
227-
# Avoiding to force the user to run a 'trigger' command
228-
trigger_screenshots(project_config, retry)
229-
sys.exit(0)
230-
if not should_wait:
231-
with tempfile.TemporaryDirectory() as patch_dir:
232-
patches = download_patches(project_config, results, patch_dir,
233-
verbose)
234-
git_apply_patch(patches, verbose)
235-
print(INFO_PATCHES_APPLIED)
236-
sys.exit(0)
237-
print(f'Waiting {wait_sec} seconds ...')
238-
time.sleep(wait_sec)
239-
apply_patch_to_local(project_config, patchset, wait_sec, ignore_failed,
240-
retry, verbose)
241-
242-
243266
def screenshot_results(project_config, patchset=None):
244267
"""Select only screenshot builders results."""
245268
results = read_try_results(patchset)
@@ -314,7 +337,7 @@ def check_not_empty(results):
314337
if results:
315338
return
316339
print('No screenshot test results found! ' +
317-
'Make sure to run `update_goldens.py trigger` first.')
340+
'Make sure to run CQ against your change first.')
318341
sys.exit(1)
319342

320343

@@ -384,48 +407,6 @@ def builder_status(results, builders):
384407
return '\n '.join(f'{b}: {results[b]["status"]}' for b in builders)
385408

386409

387-
def download_patches(project_config, results, destination_dir, verbose):
388-
"""Interact with GS and retrieve the patches. Since we have build results
389-
from successfull screenshot builds we know that they uploaded patches in
390-
the expected location in the cloud.
391-
"""
392-
patches = []
393-
for builder, result in results.items():
394-
gs_path = [
395-
project_config.gs_folder, builder,
396-
str(result['cl']),
397-
str(result['patch']), 'screenshot.patch'
398-
]
399-
gs_location = '/'.join(gs_path)
400-
patch_platform = builder.split('_')[-2]
401-
local_path = os.path.join(destination_dir, patch_platform + '.patch')
402-
if verbose:
403-
print('Downloading patch file from: ' + gs_location)
404-
run_command(gcstorage_cmd('cp', gs_location, local_path), verbose)
405-
patches.append(local_path)
406-
return patches
407-
408-
409-
def git_apply_patch(patches, verbose):
410-
"""Apply downloaded patches to the local repo."""
411-
screenshot_patches = [p for p in patches if check_patch(p)]
412-
if screenshot_patches:
413-
run_command(
414-
['git', 'apply', *screenshot_patches], verbose,
415-
'Unable to apply this patch. Maybe run "git clean" before retry.')
416-
else:
417-
print('No other changes found.')
418-
sys.exit(0)
419-
420-
421-
def check_patch(patch):
422-
"""Check if a particular patch file is empty."""
423-
if os.stat(patch).st_size == 0:
424-
print('Ignoring empty patch:%s\n' % patch)
425-
return False
426-
return True
427-
428-
429410
def run_command(command, verbose, message=None):
430411
"""Run command and deal with return code and output from the subprocess"""
431412
process = subprocess.Popen(command,
@@ -444,14 +425,8 @@ def run_command(command, verbose, message=None):
444425
sys.exit(1)
445426

446427

447-
def gcstorage_cmd(*args):
448-
return ["gcloud", "storage"] + list(args)
449-
450-
451428
if __name__ == '__main__':
452-
print(
453-
"Deprecation warning: this script is deprecated and will be removed.\n"
454-
"Please use `update_goldens_v2.py` instead.\n"
455-
"Make sure you familiarize yourself with the new script via "
456-
"`update_goldens_v2.py --help.`\n"
457-
"Note: removal pending deprecation in other projects.")
429+
main(
430+
ProjectConfig(platforms=['linux'],
431+
builder_prefix='dtf',
432+
ignore_failed_builders=True), sys.argv[1:])

0 commit comments

Comments
 (0)