aboutsummaryrefslogtreecommitdiff
path: root/src/tests/restricted_traces/restricted_trace_gold_tests.py
blob: 2708f5e3393ed24f3e5757dff34dbdb837869a31 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
#! /usr/bin/env vpython3
#
# Copyright 2020 The ANGLE Project Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# restricted_trace_gold_tests.py:
#   Uses Skia Gold (https://skia.org/dev/testing/skiagold) to run pixel tests with ANGLE traces.
#
#   Requires vpython to run standalone. Run with --help for usage instructions.

import argparse
import contextlib
import fnmatch
import json
import logging
import os
import platform
import re
import shutil
import sys
import tempfile
import time
import traceback


def _AddToPathIfNeeded(path):
    if path not in sys.path:
        sys.path.insert(0, path)


_AddToPathIfNeeded(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'py_utils')))
import android_helper
import angle_path_util
from skia_gold import angle_skia_gold_properties
from skia_gold import angle_skia_gold_session_manager

angle_path_util.AddDepsDirToPath('testing/scripts')
import common
import test_env
import xvfb


def IsWindows():
    return sys.platform == 'cygwin' or sys.platform.startswith('win')


DEFAULT_TEST_SUITE = 'angle_perftests'
DEFAULT_TEST_PREFIX = 'TracePerfTest.Run/vulkan_'
SWIFTSHADER_TEST_PREFIX = 'TracePerfTest.Run/vulkan_swiftshader_'
DEFAULT_SCREENSHOT_PREFIX = 'angle_vulkan_'
SWIFTSHADER_SCREENSHOT_PREFIX = 'angle_vulkan_swiftshader_'
DEFAULT_BATCH_SIZE = 5
DEFAULT_LOG = 'info'
DEFAULT_GOLD_INSTANCE = 'angle'

# Filters out stuff like: " I   72.572s run_tests_on_device(96071FFAZ00096) "
ANDROID_LOGGING_PREFIX = r'I +\d+.\d+s \w+\(\w+\)  '
ANDROID_BEGIN_SYSTEM_INFO = '>>ScopedMainEntryLogger'

# Test expectations
FAIL = 'FAIL'
PASS = 'PASS'
SKIP = 'SKIP'


@contextlib.contextmanager
def temporary_dir(prefix=''):
    path = tempfile.mkdtemp(prefix=prefix)
    try:
        yield path
    finally:
        logging.info("Removing temporary directory: %s" % path)
        shutil.rmtree(path)


def add_skia_gold_args(parser):
    group = parser.add_argument_group('Skia Gold Arguments')
    group.add_argument('--git-revision', help='Revision being tested.', default=None)
    group.add_argument(
        '--gerrit-issue', help='For Skia Gold integration. Gerrit issue ID.', default='')
    group.add_argument(
        '--gerrit-patchset',
        help='For Skia Gold integration. Gerrit patch set number.',
        default='')
    group.add_argument(
        '--buildbucket-id', help='For Skia Gold integration. Buildbucket build ID.', default='')
    group.add_argument(
        '--bypass-skia-gold-functionality',
        action='store_true',
        default=False,
        help='Bypass all interaction with Skia Gold, effectively disabling the '
        'image comparison portion of any tests that use Gold. Only meant to '
        'be used in case a Gold outage occurs and cannot be fixed quickly.')
    local_group = group.add_mutually_exclusive_group()
    local_group.add_argument(
        '--local-pixel-tests',
        action='store_true',
        default=None,
        help='Specifies to run the test harness in local run mode or not. When '
        'run in local mode, uploading to Gold is disabled and links to '
        'help with local debugging are output. Running in local mode also '
        'implies --no-luci-auth. If both this and --no-local-pixel-tests are '
        'left unset, the test harness will attempt to detect whether it is '
        'running on a workstation or not and set this option accordingly.')
    local_group.add_argument(
        '--no-local-pixel-tests',
        action='store_false',
        dest='local_pixel_tests',
        help='Specifies to run the test harness in non-local (bot) mode. When '
        'run in this mode, data is actually uploaded to Gold and triage links '
        'arge generated. If both this and --local-pixel-tests are left unset, '
        'the test harness will attempt to detect whether it is running on a '
        'workstation or not and set this option accordingly.')
    group.add_argument(
        '--no-luci-auth',
        action='store_true',
        default=False,
        help='Don\'t use the service account provided by LUCI for '
        'authentication for Skia Gold, instead relying on gsutil to be '
        'pre-authenticated. Meant for testing locally instead of on the bots.')


def _adb_if_android(args):
    if android_helper.ApkFileExists(args.test_suite):
        return android_helper.Adb()

    return None


def run_wrapper(test_suite, cmd_args, args, env, stdoutfile, output_dir=None):
    cmd = [get_binary_name(test_suite)] + cmd_args
    if output_dir:
        cmd += ['--render-test-output-dir=%s' % output_dir]

    if args.xvfb:
        return xvfb.run_executable(cmd, env, stdoutfile=stdoutfile)
    else:
        adb = _adb_if_android(args)
        if adb:
            try:
                android_helper.RunTests(adb, test_suite, cmd_args, stdoutfile, output_dir)
                return 0
            except Exception as e:
                logging.exception(e)
                return 1
        else:
            return test_env.run_command_with_output(cmd, env=env, stdoutfile=stdoutfile)


def run_angle_system_info_test(sysinfo_args, args, env):
    with temporary_dir() as temp_dir:
        tempfile_path = os.path.join(temp_dir, 'stdout')
        sysinfo_args += ['--render-test-output-dir=' + temp_dir]

        if run_wrapper('angle_system_info_test', sysinfo_args, args, env, tempfile_path):
            raise Exception('Error getting system info.')

        with open(os.path.join(temp_dir, 'angle_system_info.json')) as f:
            return json.load(f)


def to_hex(num):
    return hex(int(num))


def to_hex_or_none(num):
    return 'None' if num == None else to_hex(num)


def to_non_empty_string_or_none(val):
    return 'None' if val == '' else str(val)


def to_non_empty_string_or_none_dict(d, key):
    return 'None' if not key in d else to_non_empty_string_or_none(d[key])


def get_binary_name(binary):
    if IsWindows():
        return '.\\%s.exe' % binary
    else:
        return './%s' % binary


def get_skia_gold_keys(args, env):
    """Get all the JSON metadata that will be passed to golctl."""
    # All values need to be strings, otherwise goldctl fails.

    # Only call this method one time
    if hasattr(get_skia_gold_keys, 'called') and get_skia_gold_keys.called:
        logging.exception('get_skia_gold_keys may only be called once')
    get_skia_gold_keys.called = True

    sysinfo_args = ['--vulkan', '-v']
    if args.swiftshader:
        sysinfo_args.append('--swiftshader')

    adb = _adb_if_android(args)
    if adb:
        json_data = android_helper.AngleSystemInfo(adb, sysinfo_args)
        logging.info(json_data)
    else:
        json_data = run_angle_system_info_test(sysinfo_args, args, env)

    if len(json_data.get('gpus', [])) == 0 or not 'activeGPUIndex' in json_data:
        raise Exception('Error getting system info.')

    active_gpu = json_data['gpus'][json_data['activeGPUIndex']]

    angle_keys = {
        'vendor_id': to_hex_or_none(active_gpu['vendorId']),
        'device_id': to_hex_or_none(active_gpu['deviceId']),
        'model_name': to_non_empty_string_or_none_dict(active_gpu, 'machineModelVersion'),
        'manufacturer_name': to_non_empty_string_or_none_dict(active_gpu, 'machineManufacturer'),
        'os': to_non_empty_string_or_none(platform.system()),
        'os_version': to_non_empty_string_or_none(platform.version()),
        'driver_version': to_non_empty_string_or_none_dict(active_gpu, 'driverVersion'),
        'driver_vendor': to_non_empty_string_or_none_dict(active_gpu, 'driverVendor'),
    }

    return angle_keys


def output_diff_local_files(gold_session, image_name):
    """Logs the local diff image files from the given SkiaGoldSession.

  Args:
    gold_session: A skia_gold_session.SkiaGoldSession instance to pull files
        from.
    image_name: A string containing the name of the image/test that was
        compared.
  """
    given_file = gold_session.GetGivenImageLink(image_name)
    closest_file = gold_session.GetClosestImageLink(image_name)
    diff_file = gold_session.GetDiffImageLink(image_name)
    failure_message = 'Unable to retrieve link'
    logging.error('Generated image: %s', given_file or failure_message)
    logging.error('Closest image: %s', closest_file or failure_message)
    logging.error('Diff image: %s', diff_file or failure_message)


def upload_test_result_to_skia_gold(args, gold_session_manager, gold_session, gold_properties,
                                    screenshot_dir, image_name, artifacts):
    """Compares the given image using Skia Gold and uploads the result.

    No uploading is done if the test is being run in local run mode. Compares
    the given screenshot to baselines provided by Gold, raising an Exception if
    a match is not found.

    Args:
      args: Command line options.
      gold_session_manager: Skia Gold session manager.
      gold_session: Skia Gold session.
      gold_properties: Skia Gold properties.
      screenshot_dir: directory where the test stores screenshots.
      image_name: the name of the image being checked.
      artifacts: dictionary of JSON artifacts to pass to the result merger.
    """

    use_luci = not (gold_properties.local_pixel_tests or gold_properties.no_luci_auth)

    # Note: this would be better done by iterating the screenshot directory.
    prefix = SWIFTSHADER_SCREENSHOT_PREFIX if args.swiftshader else DEFAULT_SCREENSHOT_PREFIX
    png_file_name = os.path.join(screenshot_dir, prefix + image_name + '.png')

    if not os.path.isfile(png_file_name):
        raise Exception('Screenshot not found: ' + png_file_name)

    status, error = gold_session.RunComparison(
        name=image_name, png_file=png_file_name, use_luci=use_luci)

    artifact_name = os.path.basename(png_file_name)
    artifacts[artifact_name] = [artifact_name]

    if not status:
        return PASS

    status_codes = gold_session_manager.GetSessionClass().StatusCodes
    if status == status_codes.AUTH_FAILURE:
        logging.error('Gold authentication failed with output %s', error)
    elif status == status_codes.INIT_FAILURE:
        logging.error('Gold initialization failed with output %s', error)
    elif status == status_codes.COMPARISON_FAILURE_REMOTE:
        _, triage_link = gold_session.GetTriageLinks(image_name)
        if not triage_link:
            logging.error('Failed to get triage link for %s, raw output: %s', image_name, error)
            logging.error('Reason for no triage link: %s',
                          gold_session.GetTriageLinkOmissionReason(image_name))
        if gold_properties.IsTryjobRun():
            # Pick "show all results" so we can see the tryjob images by default.
            triage_link += '&master=true'
            artifacts['triage_link_for_entire_cl'] = [triage_link]
        else:
            artifacts['gold_triage_link'] = [triage_link]
    elif status == status_codes.COMPARISON_FAILURE_LOCAL:
        logging.error('Local comparison failed. Local diff files:')
        output_diff_local_files(gold_session, image_name)
    elif status == status_codes.LOCAL_DIFF_FAILURE:
        logging.error(
            'Local comparison failed and an error occurred during diff '
            'generation: %s', error)
        # There might be some files, so try outputting them.
        logging.error('Local diff files:')
        output_diff_local_files(gold_session, image_name)
    else:
        logging.error('Given unhandled SkiaGoldSession StatusCode %s with error %s', status, error)

    return FAIL


def _get_batches(traces, batch_size):
    for i in range(0, len(traces), batch_size):
        yield traces[i:i + batch_size]


def _get_gtest_filter_for_batch(args, batch):
    prefix = SWIFTSHADER_TEST_PREFIX if args.swiftshader else DEFAULT_TEST_PREFIX
    expanded = ['%s%s' % (prefix, trace) for trace in batch]
    return '--gtest_filter=%s' % ':'.join(expanded)


def _run_tests(args, tests, extra_flags, env, screenshot_dir, results, test_results):
    keys = get_skia_gold_keys(args, env)

    adb = _adb_if_android(args)
    if adb:
        android_helper.PrepareTestSuite(adb, args.test_suite)

    with temporary_dir('angle_skia_gold_') as skia_gold_temp_dir:
        gold_properties = angle_skia_gold_properties.ANGLESkiaGoldProperties(args)
        gold_session_manager = angle_skia_gold_session_manager.ANGLESkiaGoldSessionManager(
            skia_gold_temp_dir, gold_properties)
        gold_session = gold_session_manager.GetSkiaGoldSession(keys, instance=args.instance)

        traces = [trace.split(' ')[0] for trace in tests]

        if args.isolated_script_test_filter:
            filtered = []
            for trace in traces:
                # Apply test filter if present.
                full_name = 'angle_restricted_trace_gold_tests.%s' % trace
                if not fnmatch.fnmatch(full_name, args.isolated_script_test_filter):
                    logging.info('Skipping test %s because it does not match filter %s' %
                                 (full_name, args.isolated_script_test_filter))
                else:
                    filtered += [trace]
            traces = filtered

        batches = _get_batches(traces, args.batch_size)

        for batch in batches:
            if adb:
                android_helper.PrepareRestrictedTraces(adb, batch)

            for iteration in range(0, args.flaky_retries + 1):
                with common.temporary_file() as tempfile_path:
                    # This is how we signal early exit
                    if not batch:
                        logging.debug('All tests in batch completed.')
                        break
                    if iteration > 0:
                        logging.info('Test run failed, running retry #%d...' % iteration)

                    gtest_filter = _get_gtest_filter_for_batch(args, batch)
                    cmd_args = [
                        gtest_filter,
                        '--one-frame-only',
                        '--verbose-logging',
                        '--enable-all-trace-tests',
                    ] + extra_flags
                    batch_result = PASS if run_wrapper(
                        args.test_suite,
                        cmd_args,
                        args,
                        env,
                        tempfile_path,
                        output_dir=screenshot_dir) == 0 else FAIL

                    with open(tempfile_path) as f:
                        test_output = f.read() + '\n'

                    next_batch = []
                    for trace in batch:
                        artifacts = {}

                        if batch_result == PASS:
                            test_prefix = SWIFTSHADER_TEST_PREFIX if args.swiftshader else DEFAULT_TEST_PREFIX
                            trace_skipped_notice = '[  SKIPPED ] ' + test_prefix + trace + '\n'
                            if trace_skipped_notice in test_output:
                                result = SKIP
                            else:
                                logging.debug('upload test result: %s' % trace)
                                result = upload_test_result_to_skia_gold(
                                    args, gold_session_manager, gold_session, gold_properties,
                                    screenshot_dir, trace, artifacts)
                        else:
                            result = batch_result

                        expected_result = SKIP if result == SKIP else PASS
                        test_results[trace] = {'expected': expected_result, 'actual': result}
                        if len(artifacts) > 0:
                            test_results[trace]['artifacts'] = artifacts
                        if result == FAIL:
                            next_batch.append(trace)
                    batch = next_batch

        # These properties are recorded after iteration to ensure they only happen once.
        for _, trace_results in test_results.items():
            result = trace_results['actual']
            results['num_failures_by_type'][result] += 1
            if result == FAIL:
                trace_results['is_unexpected'] = True

        return results['num_failures_by_type'][FAIL] == 0


def _shard_tests(tests, shard_count, shard_index):
    return [tests[index] for index in range(shard_index, len(tests), shard_count)]


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--isolated-script-test-output', type=str)
    parser.add_argument('--isolated-script-test-perf-output', type=str)
    parser.add_argument('-f', '--isolated-script-test-filter', '--filter', type=str)
    parser.add_argument('--test-suite', help='Test suite to run.', default=DEFAULT_TEST_SUITE)
    parser.add_argument('--render-test-output-dir', help='Directory to store screenshots')
    parser.add_argument('--xvfb', help='Start xvfb.', action='store_true')
    parser.add_argument(
        '--flaky-retries', help='Number of times to retry failed tests.', type=int, default=0)
    parser.add_argument(
        '--shard-count',
        help='Number of shards for test splitting. Default is 1.',
        type=int,
        default=1)
    parser.add_argument(
        '--shard-index',
        help='Index of the current shard for test splitting. Default is 0.',
        type=int,
        default=0)
    parser.add_argument(
        '--batch-size',
        help='Number of tests to run in a group. Default: %d' % DEFAULT_BATCH_SIZE,
        type=int,
        default=DEFAULT_BATCH_SIZE)
    parser.add_argument(
        '-l', '--log', help='Log output level. Default is %s.' % DEFAULT_LOG, default=DEFAULT_LOG)
    parser.add_argument('--swiftshader', help='Test with SwiftShader.', action='store_true')
    parser.add_argument(
        '-i',
        '--instance',
        '--gold-instance',
        '--skia-gold-instance',
        help='Skia Gold instance. Default is "%s".' % DEFAULT_GOLD_INSTANCE,
        default=DEFAULT_GOLD_INSTANCE)

    add_skia_gold_args(parser)

    args, extra_flags = parser.parse_known_args()
    logging.basicConfig(level=args.log.upper())

    env = os.environ.copy()

    if 'GTEST_TOTAL_SHARDS' in env and int(env['GTEST_TOTAL_SHARDS']) != 1:
        if 'GTEST_SHARD_INDEX' not in env:
            logging.error('Sharding params must be specified together.')
            sys.exit(1)
        args.shard_count = int(env.pop('GTEST_TOTAL_SHARDS'))
        args.shard_index = int(env.pop('GTEST_SHARD_INDEX'))

    # The harness currently uploads all traces in a batch, which is very slow.
    # TODO: Reduce lag from trace uploads and remove this. http://anglebug.com/6854
    env['DEVICE_TIMEOUT_MULTIPLIER'] = '20'

    results = {
        'tests': {},
        'interrupted': False,
        'seconds_since_epoch': time.time(),
        'path_delimiter': '.',
        'version': 3,
        'num_failures_by_type': {
            FAIL: 0,
            PASS: 0,
            SKIP: 0,
        },
    }

    test_results = {}

    rc = 0

    try:
        # read test set
        json_name = os.path.join(angle_path_util.ANGLE_ROOT_DIR, 'src', 'tests',
                                 'restricted_traces', 'restricted_traces.json')
        with open(json_name) as fp:
            tests = json.load(fp)

        # Split tests according to sharding
        sharded_tests = _shard_tests(tests['traces'], args.shard_count, args.shard_index)

        if args.render_test_output_dir:
            if not _run_tests(args, sharded_tests, extra_flags, env, args.render_test_output_dir,
                              results, test_results):
                rc = 1
        elif 'ISOLATED_OUTDIR' in env:
            if not _run_tests(args, sharded_tests, extra_flags, env, env['ISOLATED_OUTDIR'],
                              results, test_results):
                rc = 1
        else:
            with temporary_dir('angle_trace_') as temp_dir:
                if not _run_tests(args, sharded_tests, extra_flags, env, temp_dir, results,
                                  test_results):
                    rc = 1

    except Exception:
        traceback.print_exc()
        results['interrupted'] = True
        rc = 1

    if test_results:
        results['tests']['angle_restricted_trace_gold_tests'] = test_results

    if args.isolated_script_test_output:
        with open(args.isolated_script_test_output, 'w') as out_file:
            out_file.write(json.dumps(results, indent=2))

    if args.isolated_script_test_perf_output:
        with open(args.isolated_script_test_perf_output, 'w') as out_file:
            out_file.write(json.dumps({}))

    return rc


# This is not really a "script test" so does not need to manually add
# any additional compile targets.
def main_compile_targets(args):
    json.dump([], args.output)


if __name__ == '__main__':
    # Conform minimally to the protocol defined by ScriptTest.
    if 'compile_targets' in sys.argv:
        funcs = {
            'run': None,
            'compile_targets': main_compile_targets,
        }
        sys.exit(common.run_script(sys.argv[1:], funcs))
    sys.exit(main())