1*3ac0a46fSAndroid Build Coastguard Worker#!/usr/bin/env python3 2*3ac0a46fSAndroid Build Coastguard Worker# Copyright 2016 The PDFium Authors 3*3ac0a46fSAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 4*3ac0a46fSAndroid Build Coastguard Worker# found in the LICENSE file. 5*3ac0a46fSAndroid Build Coastguard Worker 6*3ac0a46fSAndroid Build Coastguard Workerimport argparse 7*3ac0a46fSAndroid Build Coastguard Workerfrom dataclasses import dataclass, field 8*3ac0a46fSAndroid Build Coastguard Workerfrom datetime import timedelta 9*3ac0a46fSAndroid Build Coastguard Workerfrom io import BytesIO 10*3ac0a46fSAndroid Build Coastguard Workerimport multiprocessing 11*3ac0a46fSAndroid Build Coastguard Workerimport os 12*3ac0a46fSAndroid Build Coastguard Workerimport re 13*3ac0a46fSAndroid Build Coastguard Workerimport shutil 14*3ac0a46fSAndroid Build Coastguard Workerimport subprocess 15*3ac0a46fSAndroid Build Coastguard Workerimport sys 16*3ac0a46fSAndroid Build Coastguard Workerimport time 17*3ac0a46fSAndroid Build Coastguard Worker 18*3ac0a46fSAndroid Build Coastguard Workerimport common 19*3ac0a46fSAndroid Build Coastguard Workerimport pdfium_root 20*3ac0a46fSAndroid Build Coastguard Workerimport pngdiffer 21*3ac0a46fSAndroid Build Coastguard Workerfrom skia_gold import skia_gold 22*3ac0a46fSAndroid Build Coastguard Workerimport suppressor 23*3ac0a46fSAndroid Build Coastguard Worker 24*3ac0a46fSAndroid Build Coastguard Workerpdfium_root.add_source_directory_to_import_path(os.path.join('build', 'util')) 25*3ac0a46fSAndroid Build Coastguard Workerfrom lib.results import result_sink, result_types 26*3ac0a46fSAndroid Build Coastguard Worker 27*3ac0a46fSAndroid Build Coastguard Worker 28*3ac0a46fSAndroid Build Coastguard Worker# Arbitrary timestamp, expressed in seconds since the epoch, used to make sure 29*3ac0a46fSAndroid Build Coastguard Worker# that tests that depend on the current time are stable. Happens to be the 30*3ac0a46fSAndroid Build Coastguard Worker# timestamp of the first commit to repo, 2014/5/9 17:48:50. 31*3ac0a46fSAndroid Build Coastguard WorkerTEST_SEED_TIME = "1399672130" 32*3ac0a46fSAndroid Build Coastguard Worker 33*3ac0a46fSAndroid Build Coastguard Worker# List of test types that should run text tests instead of pixel tests. 34*3ac0a46fSAndroid Build Coastguard WorkerTEXT_TESTS = ['javascript'] 35*3ac0a46fSAndroid Build Coastguard Worker 36*3ac0a46fSAndroid Build Coastguard Worker# Timeout (in seconds) for individual test commands. 37*3ac0a46fSAndroid Build Coastguard Worker# TODO(crbug.com/pdfium/1967): array_buffer.in is slow under MSan, so need a 38*3ac0a46fSAndroid Build Coastguard Worker# very generous 5 minute timeout for now. 39*3ac0a46fSAndroid Build Coastguard WorkerTEST_TIMEOUT = timedelta(minutes=5).total_seconds() 40*3ac0a46fSAndroid Build Coastguard Worker 41*3ac0a46fSAndroid Build Coastguard Worker 42*3ac0a46fSAndroid Build Coastguard Workerclass TestRunner: 43*3ac0a46fSAndroid Build Coastguard Worker 44*3ac0a46fSAndroid Build Coastguard Worker def __init__(self, dirname): 45*3ac0a46fSAndroid Build Coastguard Worker # Currently the only used directories are corpus, javascript, and pixel, 46*3ac0a46fSAndroid Build Coastguard Worker # which all correspond directly to the type for the test being run. In the 47*3ac0a46fSAndroid Build Coastguard Worker # future if there are tests that don't have this clean correspondence, then 48*3ac0a46fSAndroid Build Coastguard Worker # an argument for the type will need to be added. 49*3ac0a46fSAndroid Build Coastguard Worker self.per_process_config = _PerProcessConfig( 50*3ac0a46fSAndroid Build Coastguard Worker test_dir=dirname, test_type=dirname) 51*3ac0a46fSAndroid Build Coastguard Worker 52*3ac0a46fSAndroid Build Coastguard Worker @property 53*3ac0a46fSAndroid Build Coastguard Worker def options(self): 54*3ac0a46fSAndroid Build Coastguard Worker return self.per_process_config.options 55*3ac0a46fSAndroid Build Coastguard Worker 56*3ac0a46fSAndroid Build Coastguard Worker def IsSkiaGoldEnabled(self): 57*3ac0a46fSAndroid Build Coastguard Worker return (self.options.run_skia_gold and 58*3ac0a46fSAndroid Build Coastguard Worker not self.per_process_config.test_type in TEXT_TESTS) 59*3ac0a46fSAndroid Build Coastguard Worker 60*3ac0a46fSAndroid Build Coastguard Worker def IsExecutionSuppressed(self, input_path): 61*3ac0a46fSAndroid Build Coastguard Worker return self.per_process_state.test_suppressor.IsExecutionSuppressed( 62*3ac0a46fSAndroid Build Coastguard Worker input_path) 63*3ac0a46fSAndroid Build Coastguard Worker 64*3ac0a46fSAndroid Build Coastguard Worker def IsResultSuppressed(self, input_filename): 65*3ac0a46fSAndroid Build Coastguard Worker return self.per_process_state.test_suppressor.IsResultSuppressed( 66*3ac0a46fSAndroid Build Coastguard Worker input_filename) 67*3ac0a46fSAndroid Build Coastguard Worker 68*3ac0a46fSAndroid Build Coastguard Worker def HandleResult(self, test_case, test_result): 69*3ac0a46fSAndroid Build Coastguard Worker input_filename = os.path.basename(test_case.input_path) 70*3ac0a46fSAndroid Build Coastguard Worker 71*3ac0a46fSAndroid Build Coastguard Worker test_result.status = self._SuppressStatus(input_filename, 72*3ac0a46fSAndroid Build Coastguard Worker test_result.status) 73*3ac0a46fSAndroid Build Coastguard Worker if test_result.status == result_types.UNKNOWN: 74*3ac0a46fSAndroid Build Coastguard Worker self.result_suppressed_cases.append(input_filename) 75*3ac0a46fSAndroid Build Coastguard Worker self.surprises.append(test_case.input_path) 76*3ac0a46fSAndroid Build Coastguard Worker elif test_result.status == result_types.SKIP: 77*3ac0a46fSAndroid Build Coastguard Worker self.result_suppressed_cases.append(input_filename) 78*3ac0a46fSAndroid Build Coastguard Worker elif not test_result.IsPass(): 79*3ac0a46fSAndroid Build Coastguard Worker self.failures.append(test_case.input_path) 80*3ac0a46fSAndroid Build Coastguard Worker 81*3ac0a46fSAndroid Build Coastguard Worker for artifact in test_result.image_artifacts: 82*3ac0a46fSAndroid Build Coastguard Worker if artifact.skia_gold_status == result_types.PASS: 83*3ac0a46fSAndroid Build Coastguard Worker if self.IsResultSuppressed(artifact.image_path): 84*3ac0a46fSAndroid Build Coastguard Worker self.skia_gold_unexpected_successes.append(artifact.GetSkiaGoldId()) 85*3ac0a46fSAndroid Build Coastguard Worker else: 86*3ac0a46fSAndroid Build Coastguard Worker self.skia_gold_successes.append(artifact.GetSkiaGoldId()) 87*3ac0a46fSAndroid Build Coastguard Worker elif artifact.skia_gold_status == result_types.FAIL: 88*3ac0a46fSAndroid Build Coastguard Worker self.skia_gold_failures.append(artifact.GetSkiaGoldId()) 89*3ac0a46fSAndroid Build Coastguard Worker 90*3ac0a46fSAndroid Build Coastguard Worker # Log test result. 91*3ac0a46fSAndroid Build Coastguard Worker print(f'{test_result.status}: {test_result.test_id}') 92*3ac0a46fSAndroid Build Coastguard Worker if not test_result.IsPass(): 93*3ac0a46fSAndroid Build Coastguard Worker if test_result.reason: 94*3ac0a46fSAndroid Build Coastguard Worker print(f'Failure reason: {test_result.reason}') 95*3ac0a46fSAndroid Build Coastguard Worker if test_result.log: 96*3ac0a46fSAndroid Build Coastguard Worker print(f'Test output:\n{test_result.log}') 97*3ac0a46fSAndroid Build Coastguard Worker for artifact in test_result.image_artifacts: 98*3ac0a46fSAndroid Build Coastguard Worker if artifact.skia_gold_status == result_types.FAIL: 99*3ac0a46fSAndroid Build Coastguard Worker print(f'Failed Skia Gold: {artifact.image_path}') 100*3ac0a46fSAndroid Build Coastguard Worker if artifact.image_diff: 101*3ac0a46fSAndroid Build Coastguard Worker print(f'Failed image diff: {artifact.image_diff.reason}') 102*3ac0a46fSAndroid Build Coastguard Worker 103*3ac0a46fSAndroid Build Coastguard Worker # Report test result to ResultDB. 104*3ac0a46fSAndroid Build Coastguard Worker if self.resultdb: 105*3ac0a46fSAndroid Build Coastguard Worker only_artifacts = None 106*3ac0a46fSAndroid Build Coastguard Worker only_failure_reason = test_result.reason 107*3ac0a46fSAndroid Build Coastguard Worker if len(test_result.image_artifacts) == 1: 108*3ac0a46fSAndroid Build Coastguard Worker only = test_result.image_artifacts[0] 109*3ac0a46fSAndroid Build Coastguard Worker only_artifacts = only.GetDiffArtifacts() 110*3ac0a46fSAndroid Build Coastguard Worker if only.GetDiffReason(): 111*3ac0a46fSAndroid Build Coastguard Worker only_failure_reason += f': {only.GetDiffReason()}' 112*3ac0a46fSAndroid Build Coastguard Worker self.resultdb.Post( 113*3ac0a46fSAndroid Build Coastguard Worker test_id=test_result.test_id, 114*3ac0a46fSAndroid Build Coastguard Worker status=test_result.status, 115*3ac0a46fSAndroid Build Coastguard Worker duration=test_result.duration_milliseconds, 116*3ac0a46fSAndroid Build Coastguard Worker test_log=test_result.log, 117*3ac0a46fSAndroid Build Coastguard Worker test_file=None, 118*3ac0a46fSAndroid Build Coastguard Worker artifacts=only_artifacts, 119*3ac0a46fSAndroid Build Coastguard Worker failure_reason=only_failure_reason) 120*3ac0a46fSAndroid Build Coastguard Worker 121*3ac0a46fSAndroid Build Coastguard Worker # Milo only supports a single diff per test, so if we have multiple pages, 122*3ac0a46fSAndroid Build Coastguard Worker # report each page as its own "test." 123*3ac0a46fSAndroid Build Coastguard Worker if len(test_result.image_artifacts) > 1: 124*3ac0a46fSAndroid Build Coastguard Worker for page, artifact in enumerate(test_result.image_artifacts): 125*3ac0a46fSAndroid Build Coastguard Worker self.resultdb.Post( 126*3ac0a46fSAndroid Build Coastguard Worker test_id=f'{test_result.test_id}/{page}', 127*3ac0a46fSAndroid Build Coastguard Worker status=self._SuppressArtifactStatus(test_result, 128*3ac0a46fSAndroid Build Coastguard Worker artifact.GetDiffStatus()), 129*3ac0a46fSAndroid Build Coastguard Worker duration=None, 130*3ac0a46fSAndroid Build Coastguard Worker test_log=None, 131*3ac0a46fSAndroid Build Coastguard Worker test_file=None, 132*3ac0a46fSAndroid Build Coastguard Worker artifacts=artifact.GetDiffArtifacts(), 133*3ac0a46fSAndroid Build Coastguard Worker failure_reason=artifact.GetDiffReason()) 134*3ac0a46fSAndroid Build Coastguard Worker 135*3ac0a46fSAndroid Build Coastguard Worker def _SuppressStatus(self, input_filename, status): 136*3ac0a46fSAndroid Build Coastguard Worker if not self.IsResultSuppressed(input_filename): 137*3ac0a46fSAndroid Build Coastguard Worker return status 138*3ac0a46fSAndroid Build Coastguard Worker 139*3ac0a46fSAndroid Build Coastguard Worker if status == result_types.PASS: 140*3ac0a46fSAndroid Build Coastguard Worker # There isn't an actual status for succeeded-but-ignored, so use the 141*3ac0a46fSAndroid Build Coastguard Worker # "abort" status to differentiate this from failed-but-ignored. 142*3ac0a46fSAndroid Build Coastguard Worker # 143*3ac0a46fSAndroid Build Coastguard Worker # Note that this appears as a preliminary failure in Gerrit. 144*3ac0a46fSAndroid Build Coastguard Worker return result_types.UNKNOWN 145*3ac0a46fSAndroid Build Coastguard Worker 146*3ac0a46fSAndroid Build Coastguard Worker # There isn't an actual status for failed-but-ignored, so use the "skip" 147*3ac0a46fSAndroid Build Coastguard Worker # status to differentiate this from succeeded-but-ignored. 148*3ac0a46fSAndroid Build Coastguard Worker return result_types.SKIP 149*3ac0a46fSAndroid Build Coastguard Worker 150*3ac0a46fSAndroid Build Coastguard Worker def _SuppressArtifactStatus(self, test_result, status): 151*3ac0a46fSAndroid Build Coastguard Worker if status != result_types.FAIL: 152*3ac0a46fSAndroid Build Coastguard Worker return status 153*3ac0a46fSAndroid Build Coastguard Worker 154*3ac0a46fSAndroid Build Coastguard Worker if test_result.status != result_types.SKIP: 155*3ac0a46fSAndroid Build Coastguard Worker return status 156*3ac0a46fSAndroid Build Coastguard Worker 157*3ac0a46fSAndroid Build Coastguard Worker return result_types.SKIP 158*3ac0a46fSAndroid Build Coastguard Worker 159*3ac0a46fSAndroid Build Coastguard Worker def Run(self): 160*3ac0a46fSAndroid Build Coastguard Worker # Running a test defines a number of attributes on the fly. 161*3ac0a46fSAndroid Build Coastguard Worker # pylint: disable=attribute-defined-outside-init 162*3ac0a46fSAndroid Build Coastguard Worker 163*3ac0a46fSAndroid Build Coastguard Worker relative_test_dir = self.per_process_config.test_dir 164*3ac0a46fSAndroid Build Coastguard Worker if relative_test_dir != 'corpus': 165*3ac0a46fSAndroid Build Coastguard Worker relative_test_dir = os.path.join('resources', relative_test_dir) 166*3ac0a46fSAndroid Build Coastguard Worker 167*3ac0a46fSAndroid Build Coastguard Worker parser = argparse.ArgumentParser() 168*3ac0a46fSAndroid Build Coastguard Worker 169*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 170*3ac0a46fSAndroid Build Coastguard Worker '--build-dir', 171*3ac0a46fSAndroid Build Coastguard Worker default=os.path.join('out', 'Debug'), 172*3ac0a46fSAndroid Build Coastguard Worker help='relative path from the base source directory') 173*3ac0a46fSAndroid Build Coastguard Worker 174*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 175*3ac0a46fSAndroid Build Coastguard Worker '-j', 176*3ac0a46fSAndroid Build Coastguard Worker default=multiprocessing.cpu_count(), 177*3ac0a46fSAndroid Build Coastguard Worker dest='num_workers', 178*3ac0a46fSAndroid Build Coastguard Worker type=int, 179*3ac0a46fSAndroid Build Coastguard Worker help='run NUM_WORKERS jobs in parallel') 180*3ac0a46fSAndroid Build Coastguard Worker 181*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 182*3ac0a46fSAndroid Build Coastguard Worker '--disable-javascript', 183*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 184*3ac0a46fSAndroid Build Coastguard Worker help='Prevents JavaScript from executing in PDF files.') 185*3ac0a46fSAndroid Build Coastguard Worker 186*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 187*3ac0a46fSAndroid Build Coastguard Worker '--disable-xfa', 188*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 189*3ac0a46fSAndroid Build Coastguard Worker help='Prevents processing XFA forms.') 190*3ac0a46fSAndroid Build Coastguard Worker 191*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 192*3ac0a46fSAndroid Build Coastguard Worker '--render-oneshot', 193*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 194*3ac0a46fSAndroid Build Coastguard Worker help='Sets whether to use the oneshot renderer.') 195*3ac0a46fSAndroid Build Coastguard Worker 196*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 197*3ac0a46fSAndroid Build Coastguard Worker '--run-skia-gold', 198*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 199*3ac0a46fSAndroid Build Coastguard Worker default=False, 200*3ac0a46fSAndroid Build Coastguard Worker help='When flag is on, skia gold tests will be run.') 201*3ac0a46fSAndroid Build Coastguard Worker 202*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 203*3ac0a46fSAndroid Build Coastguard Worker '--regenerate_expected', 204*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 205*3ac0a46fSAndroid Build Coastguard Worker help='Regenerates expected images. For each failing image diff, this ' 206*3ac0a46fSAndroid Build Coastguard Worker 'will regenerate the most specific expected image file that exists. ' 207*3ac0a46fSAndroid Build Coastguard Worker 'This also will suggest removals of unnecessary expected image files ' 208*3ac0a46fSAndroid Build Coastguard Worker 'by renaming them with an additional ".bak" extension, although these ' 209*3ac0a46fSAndroid Build Coastguard Worker 'removals should be reviewed manually. Use "git clean" to quickly deal ' 210*3ac0a46fSAndroid Build Coastguard Worker 'with any ".bak" files.') 211*3ac0a46fSAndroid Build Coastguard Worker 212*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 213*3ac0a46fSAndroid Build Coastguard Worker '--reverse-byte-order', 214*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 215*3ac0a46fSAndroid Build Coastguard Worker help='Run image-based tests using --reverse-byte-order.') 216*3ac0a46fSAndroid Build Coastguard Worker 217*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 218*3ac0a46fSAndroid Build Coastguard Worker '--ignore_errors', 219*3ac0a46fSAndroid Build Coastguard Worker action='store_true', 220*3ac0a46fSAndroid Build Coastguard Worker help='Prevents the return value from being non-zero ' 221*3ac0a46fSAndroid Build Coastguard Worker 'when image comparison fails.') 222*3ac0a46fSAndroid Build Coastguard Worker 223*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 224*3ac0a46fSAndroid Build Coastguard Worker '--use-renderer', 225*3ac0a46fSAndroid Build Coastguard Worker choices=('agg', 'gdi', 'skia'), 226*3ac0a46fSAndroid Build Coastguard Worker help='Forces the renderer to use.') 227*3ac0a46fSAndroid Build Coastguard Worker 228*3ac0a46fSAndroid Build Coastguard Worker parser.add_argument( 229*3ac0a46fSAndroid Build Coastguard Worker 'inputted_file_paths', 230*3ac0a46fSAndroid Build Coastguard Worker nargs='*', 231*3ac0a46fSAndroid Build Coastguard Worker help='Path to test files to run, relative to ' 232*3ac0a46fSAndroid Build Coastguard Worker f'testing/{relative_test_dir}. If omitted, runs all test files under ' 233*3ac0a46fSAndroid Build Coastguard Worker f'testing/{relative_test_dir}.', 234*3ac0a46fSAndroid Build Coastguard Worker metavar='relative/test/path') 235*3ac0a46fSAndroid Build Coastguard Worker 236*3ac0a46fSAndroid Build Coastguard Worker skia_gold.add_skia_gold_args(parser) 237*3ac0a46fSAndroid Build Coastguard Worker 238*3ac0a46fSAndroid Build Coastguard Worker self.per_process_config.options = parser.parse_args() 239*3ac0a46fSAndroid Build Coastguard Worker 240*3ac0a46fSAndroid Build Coastguard Worker finder = self.per_process_config.NewFinder() 241*3ac0a46fSAndroid Build Coastguard Worker pdfium_test_path = self.per_process_config.GetPdfiumTestPath(finder) 242*3ac0a46fSAndroid Build Coastguard Worker if not os.path.exists(pdfium_test_path): 243*3ac0a46fSAndroid Build Coastguard Worker print(f"FAILURE: Can't find test executable '{pdfium_test_path}'") 244*3ac0a46fSAndroid Build Coastguard Worker print('Use --build-dir to specify its location.') 245*3ac0a46fSAndroid Build Coastguard Worker return 1 246*3ac0a46fSAndroid Build Coastguard Worker 247*3ac0a46fSAndroid Build Coastguard Worker error_message = self.per_process_config.InitializeFeatures(pdfium_test_path) 248*3ac0a46fSAndroid Build Coastguard Worker if error_message: 249*3ac0a46fSAndroid Build Coastguard Worker print('FAILURE:', error_message) 250*3ac0a46fSAndroid Build Coastguard Worker return 1 251*3ac0a46fSAndroid Build Coastguard Worker 252*3ac0a46fSAndroid Build Coastguard Worker self.per_process_state = _PerProcessState(self.per_process_config) 253*3ac0a46fSAndroid Build Coastguard Worker shutil.rmtree(self.per_process_state.working_dir, ignore_errors=True) 254*3ac0a46fSAndroid Build Coastguard Worker os.makedirs(self.per_process_state.working_dir) 255*3ac0a46fSAndroid Build Coastguard Worker 256*3ac0a46fSAndroid Build Coastguard Worker error_message = self.per_process_state.image_differ.CheckMissingTools( 257*3ac0a46fSAndroid Build Coastguard Worker self.options.regenerate_expected) 258*3ac0a46fSAndroid Build Coastguard Worker if error_message: 259*3ac0a46fSAndroid Build Coastguard Worker print('FAILURE:', error_message) 260*3ac0a46fSAndroid Build Coastguard Worker return 1 261*3ac0a46fSAndroid Build Coastguard Worker 262*3ac0a46fSAndroid Build Coastguard Worker self.resultdb = result_sink.TryInitClient() 263*3ac0a46fSAndroid Build Coastguard Worker if self.resultdb: 264*3ac0a46fSAndroid Build Coastguard Worker print('Detected ResultSink environment') 265*3ac0a46fSAndroid Build Coastguard Worker 266*3ac0a46fSAndroid Build Coastguard Worker # Collect test cases. 267*3ac0a46fSAndroid Build Coastguard Worker walk_from_dir = finder.TestingDir(relative_test_dir) 268*3ac0a46fSAndroid Build Coastguard Worker 269*3ac0a46fSAndroid Build Coastguard Worker self.test_cases = TestCaseManager() 270*3ac0a46fSAndroid Build Coastguard Worker self.execution_suppressed_cases = [] 271*3ac0a46fSAndroid Build Coastguard Worker input_file_re = re.compile('^.+[.](in|pdf)$') 272*3ac0a46fSAndroid Build Coastguard Worker if self.options.inputted_file_paths: 273*3ac0a46fSAndroid Build Coastguard Worker for file_name in self.options.inputted_file_paths: 274*3ac0a46fSAndroid Build Coastguard Worker input_path = os.path.join(walk_from_dir, file_name) 275*3ac0a46fSAndroid Build Coastguard Worker if not os.path.isfile(input_path): 276*3ac0a46fSAndroid Build Coastguard Worker print(f"Can't find test file '{file_name}'") 277*3ac0a46fSAndroid Build Coastguard Worker return 1 278*3ac0a46fSAndroid Build Coastguard Worker 279*3ac0a46fSAndroid Build Coastguard Worker self.test_cases.NewTestCase(input_path) 280*3ac0a46fSAndroid Build Coastguard Worker else: 281*3ac0a46fSAndroid Build Coastguard Worker for file_dir, _, filename_list in os.walk(walk_from_dir): 282*3ac0a46fSAndroid Build Coastguard Worker for input_filename in filename_list: 283*3ac0a46fSAndroid Build Coastguard Worker if input_file_re.match(input_filename): 284*3ac0a46fSAndroid Build Coastguard Worker input_path = os.path.join(file_dir, input_filename) 285*3ac0a46fSAndroid Build Coastguard Worker if self.IsExecutionSuppressed(input_path): 286*3ac0a46fSAndroid Build Coastguard Worker self.execution_suppressed_cases.append(input_path) 287*3ac0a46fSAndroid Build Coastguard Worker continue 288*3ac0a46fSAndroid Build Coastguard Worker if not os.path.isfile(input_path): 289*3ac0a46fSAndroid Build Coastguard Worker continue 290*3ac0a46fSAndroid Build Coastguard Worker 291*3ac0a46fSAndroid Build Coastguard Worker self.test_cases.NewTestCase(input_path) 292*3ac0a46fSAndroid Build Coastguard Worker 293*3ac0a46fSAndroid Build Coastguard Worker # Execute test cases. 294*3ac0a46fSAndroid Build Coastguard Worker self.failures = [] 295*3ac0a46fSAndroid Build Coastguard Worker self.surprises = [] 296*3ac0a46fSAndroid Build Coastguard Worker self.skia_gold_successes = [] 297*3ac0a46fSAndroid Build Coastguard Worker self.skia_gold_unexpected_successes = [] 298*3ac0a46fSAndroid Build Coastguard Worker self.skia_gold_failures = [] 299*3ac0a46fSAndroid Build Coastguard Worker self.result_suppressed_cases = [] 300*3ac0a46fSAndroid Build Coastguard Worker 301*3ac0a46fSAndroid Build Coastguard Worker if self.IsSkiaGoldEnabled(): 302*3ac0a46fSAndroid Build Coastguard Worker assert self.options.gold_output_dir 303*3ac0a46fSAndroid Build Coastguard Worker # Clear out and create top level gold output directory before starting 304*3ac0a46fSAndroid Build Coastguard Worker skia_gold.clear_gold_output_dir(self.options.gold_output_dir) 305*3ac0a46fSAndroid Build Coastguard Worker 306*3ac0a46fSAndroid Build Coastguard Worker with multiprocessing.Pool( 307*3ac0a46fSAndroid Build Coastguard Worker processes=self.options.num_workers, 308*3ac0a46fSAndroid Build Coastguard Worker initializer=_InitializePerProcessState, 309*3ac0a46fSAndroid Build Coastguard Worker initargs=[self.per_process_config]) as pool: 310*3ac0a46fSAndroid Build Coastguard Worker if self.per_process_config.test_type in TEXT_TESTS: 311*3ac0a46fSAndroid Build Coastguard Worker test_function = _RunTextTest 312*3ac0a46fSAndroid Build Coastguard Worker else: 313*3ac0a46fSAndroid Build Coastguard Worker test_function = _RunPixelTest 314*3ac0a46fSAndroid Build Coastguard Worker for result in pool.imap(test_function, self.test_cases): 315*3ac0a46fSAndroid Build Coastguard Worker self.HandleResult(self.test_cases.GetTestCase(result.test_id), result) 316*3ac0a46fSAndroid Build Coastguard Worker 317*3ac0a46fSAndroid Build Coastguard Worker # Report test results. 318*3ac0a46fSAndroid Build Coastguard Worker if self.surprises: 319*3ac0a46fSAndroid Build Coastguard Worker self.surprises.sort() 320*3ac0a46fSAndroid Build Coastguard Worker print('\nUnexpected Successes:') 321*3ac0a46fSAndroid Build Coastguard Worker for surprise in self.surprises: 322*3ac0a46fSAndroid Build Coastguard Worker print(surprise) 323*3ac0a46fSAndroid Build Coastguard Worker 324*3ac0a46fSAndroid Build Coastguard Worker if self.failures: 325*3ac0a46fSAndroid Build Coastguard Worker self.failures.sort() 326*3ac0a46fSAndroid Build Coastguard Worker print('\nSummary of Failures:') 327*3ac0a46fSAndroid Build Coastguard Worker for failure in self.failures: 328*3ac0a46fSAndroid Build Coastguard Worker print(failure) 329*3ac0a46fSAndroid Build Coastguard Worker 330*3ac0a46fSAndroid Build Coastguard Worker if self.skia_gold_unexpected_successes: 331*3ac0a46fSAndroid Build Coastguard Worker self.skia_gold_unexpected_successes.sort() 332*3ac0a46fSAndroid Build Coastguard Worker print('\nUnexpected Skia Gold Successes:') 333*3ac0a46fSAndroid Build Coastguard Worker for surprise in self.skia_gold_unexpected_successes: 334*3ac0a46fSAndroid Build Coastguard Worker print(surprise) 335*3ac0a46fSAndroid Build Coastguard Worker 336*3ac0a46fSAndroid Build Coastguard Worker if self.skia_gold_failures: 337*3ac0a46fSAndroid Build Coastguard Worker self.skia_gold_failures.sort() 338*3ac0a46fSAndroid Build Coastguard Worker print('\nSummary of Skia Gold Failures:') 339*3ac0a46fSAndroid Build Coastguard Worker for failure in self.skia_gold_failures: 340*3ac0a46fSAndroid Build Coastguard Worker print(failure) 341*3ac0a46fSAndroid Build Coastguard Worker 342*3ac0a46fSAndroid Build Coastguard Worker self._PrintSummary() 343*3ac0a46fSAndroid Build Coastguard Worker 344*3ac0a46fSAndroid Build Coastguard Worker if self.failures: 345*3ac0a46fSAndroid Build Coastguard Worker if not self.options.ignore_errors: 346*3ac0a46fSAndroid Build Coastguard Worker return 1 347*3ac0a46fSAndroid Build Coastguard Worker 348*3ac0a46fSAndroid Build Coastguard Worker return 0 349*3ac0a46fSAndroid Build Coastguard Worker 350*3ac0a46fSAndroid Build Coastguard Worker def _PrintSummary(self): 351*3ac0a46fSAndroid Build Coastguard Worker number_test_cases = len(self.test_cases) 352*3ac0a46fSAndroid Build Coastguard Worker number_failures = len(self.failures) 353*3ac0a46fSAndroid Build Coastguard Worker number_suppressed = len(self.result_suppressed_cases) 354*3ac0a46fSAndroid Build Coastguard Worker number_successes = number_test_cases - number_failures - number_suppressed 355*3ac0a46fSAndroid Build Coastguard Worker number_surprises = len(self.surprises) 356*3ac0a46fSAndroid Build Coastguard Worker print('\nTest cases executed:', number_test_cases) 357*3ac0a46fSAndroid Build Coastguard Worker print(' Successes:', number_successes) 358*3ac0a46fSAndroid Build Coastguard Worker print(' Suppressed:', number_suppressed) 359*3ac0a46fSAndroid Build Coastguard Worker print(' Surprises:', number_surprises) 360*3ac0a46fSAndroid Build Coastguard Worker print(' Failures:', number_failures) 361*3ac0a46fSAndroid Build Coastguard Worker if self.IsSkiaGoldEnabled(): 362*3ac0a46fSAndroid Build Coastguard Worker number_gold_failures = len(self.skia_gold_failures) 363*3ac0a46fSAndroid Build Coastguard Worker number_gold_successes = len(self.skia_gold_successes) 364*3ac0a46fSAndroid Build Coastguard Worker number_gold_surprises = len(self.skia_gold_unexpected_successes) 365*3ac0a46fSAndroid Build Coastguard Worker number_total_gold_tests = sum( 366*3ac0a46fSAndroid Build Coastguard Worker [number_gold_failures, number_gold_successes, number_gold_surprises]) 367*3ac0a46fSAndroid Build Coastguard Worker print('\nSkia Gold Test cases executed:', number_total_gold_tests) 368*3ac0a46fSAndroid Build Coastguard Worker print(' Skia Gold Successes:', number_gold_successes) 369*3ac0a46fSAndroid Build Coastguard Worker print(' Skia Gold Surprises:', number_gold_surprises) 370*3ac0a46fSAndroid Build Coastguard Worker print(' Skia Gold Failures:', number_gold_failures) 371*3ac0a46fSAndroid Build Coastguard Worker skia_tester = self.per_process_state.GetSkiaGoldTester() 372*3ac0a46fSAndroid Build Coastguard Worker if self.skia_gold_failures and skia_tester.IsTryjobRun(): 373*3ac0a46fSAndroid Build Coastguard Worker cl_triage_link = skia_tester.GetCLTriageLink() 374*3ac0a46fSAndroid Build Coastguard Worker print(' Triage link for CL:', cl_triage_link) 375*3ac0a46fSAndroid Build Coastguard Worker skia_tester.WriteCLTriageLink(cl_triage_link) 376*3ac0a46fSAndroid Build Coastguard Worker print() 377*3ac0a46fSAndroid Build Coastguard Worker print('Test cases not executed:', len(self.execution_suppressed_cases)) 378*3ac0a46fSAndroid Build Coastguard Worker 379*3ac0a46fSAndroid Build Coastguard Worker def SetDeleteOutputOnSuccess(self, new_value): 380*3ac0a46fSAndroid Build Coastguard Worker """Set whether to delete generated output if the test passes.""" 381*3ac0a46fSAndroid Build Coastguard Worker self.per_process_config.delete_output_on_success = new_value 382*3ac0a46fSAndroid Build Coastguard Worker 383*3ac0a46fSAndroid Build Coastguard Worker def SetEnforceExpectedImages(self, new_value): 384*3ac0a46fSAndroid Build Coastguard Worker """Set whether to enforce that each test case provide an expected image.""" 385*3ac0a46fSAndroid Build Coastguard Worker self.per_process_config.enforce_expected_images = new_value 386*3ac0a46fSAndroid Build Coastguard Worker 387*3ac0a46fSAndroid Build Coastguard Worker 388*3ac0a46fSAndroid Build Coastguard Workerdef _RunTextTest(test_case): 389*3ac0a46fSAndroid Build Coastguard Worker """Runs a text test case.""" 390*3ac0a46fSAndroid Build Coastguard Worker test_case_runner = _TestCaseRunner(test_case) 391*3ac0a46fSAndroid Build Coastguard Worker with test_case_runner: 392*3ac0a46fSAndroid Build Coastguard Worker test_case_runner.test_result = test_case_runner.GenerateAndTest( 393*3ac0a46fSAndroid Build Coastguard Worker test_case_runner.TestText) 394*3ac0a46fSAndroid Build Coastguard Worker return test_case_runner.test_result 395*3ac0a46fSAndroid Build Coastguard Worker 396*3ac0a46fSAndroid Build Coastguard Worker 397*3ac0a46fSAndroid Build Coastguard Workerdef _RunPixelTest(test_case): 398*3ac0a46fSAndroid Build Coastguard Worker """Runs a pixel test case.""" 399*3ac0a46fSAndroid Build Coastguard Worker test_case_runner = _TestCaseRunner(test_case) 400*3ac0a46fSAndroid Build Coastguard Worker with test_case_runner: 401*3ac0a46fSAndroid Build Coastguard Worker test_case_runner.test_result = test_case_runner.GenerateAndTest( 402*3ac0a46fSAndroid Build Coastguard Worker test_case_runner.TestPixel) 403*3ac0a46fSAndroid Build Coastguard Worker return test_case_runner.test_result 404*3ac0a46fSAndroid Build Coastguard Worker 405*3ac0a46fSAndroid Build Coastguard Worker 406*3ac0a46fSAndroid Build Coastguard Worker# `_PerProcessState` singleton. This is initialized when creating the 407*3ac0a46fSAndroid Build Coastguard Worker# `multiprocessing.Pool()`. `TestRunner.Run()` creates its own separate 408*3ac0a46fSAndroid Build Coastguard Worker# instance of `_PerProcessState` as well. 409*3ac0a46fSAndroid Build Coastguard Worker_per_process_state = None 410*3ac0a46fSAndroid Build Coastguard Worker 411*3ac0a46fSAndroid Build Coastguard Worker 412*3ac0a46fSAndroid Build Coastguard Workerdef _InitializePerProcessState(config): 413*3ac0a46fSAndroid Build Coastguard Worker """Initializes the `_per_process_state` singleton.""" 414*3ac0a46fSAndroid Build Coastguard Worker global _per_process_state 415*3ac0a46fSAndroid Build Coastguard Worker assert not _per_process_state 416*3ac0a46fSAndroid Build Coastguard Worker _per_process_state = _PerProcessState(config) 417*3ac0a46fSAndroid Build Coastguard Worker 418*3ac0a46fSAndroid Build Coastguard Worker 419*3ac0a46fSAndroid Build Coastguard Worker@dataclass 420*3ac0a46fSAndroid Build Coastguard Workerclass _PerProcessConfig: 421*3ac0a46fSAndroid Build Coastguard Worker """Configuration for initializing `_PerProcessState`. 422*3ac0a46fSAndroid Build Coastguard Worker 423*3ac0a46fSAndroid Build Coastguard Worker Attributes: 424*3ac0a46fSAndroid Build Coastguard Worker test_dir: The name of the test directory. 425*3ac0a46fSAndroid Build Coastguard Worker test_type: The test type. 426*3ac0a46fSAndroid Build Coastguard Worker delete_output_on_success: Whether to delete output on success. 427*3ac0a46fSAndroid Build Coastguard Worker enforce_expected_images: Whether to enforce expected images. 428*3ac0a46fSAndroid Build Coastguard Worker options: The dictionary of command line options. 429*3ac0a46fSAndroid Build Coastguard Worker features: The set of features supported by `pdfium_test`. 430*3ac0a46fSAndroid Build Coastguard Worker rendering_option: The renderer to use (agg, gdi, or skia). 431*3ac0a46fSAndroid Build Coastguard Worker """ 432*3ac0a46fSAndroid Build Coastguard Worker test_dir: str 433*3ac0a46fSAndroid Build Coastguard Worker test_type: str 434*3ac0a46fSAndroid Build Coastguard Worker delete_output_on_success: bool = False 435*3ac0a46fSAndroid Build Coastguard Worker enforce_expected_images: bool = False 436*3ac0a46fSAndroid Build Coastguard Worker options: dict = None 437*3ac0a46fSAndroid Build Coastguard Worker features: set = None 438*3ac0a46fSAndroid Build Coastguard Worker rendering_option: str = None 439*3ac0a46fSAndroid Build Coastguard Worker 440*3ac0a46fSAndroid Build Coastguard Worker def NewFinder(self): 441*3ac0a46fSAndroid Build Coastguard Worker return common.DirectoryFinder(self.options.build_dir) 442*3ac0a46fSAndroid Build Coastguard Worker 443*3ac0a46fSAndroid Build Coastguard Worker def GetPdfiumTestPath(self, finder): 444*3ac0a46fSAndroid Build Coastguard Worker return finder.ExecutablePath('pdfium_test') 445*3ac0a46fSAndroid Build Coastguard Worker 446*3ac0a46fSAndroid Build Coastguard Worker def InitializeFeatures(self, pdfium_test_path): 447*3ac0a46fSAndroid Build Coastguard Worker output = subprocess.check_output([pdfium_test_path, '--show-config'], 448*3ac0a46fSAndroid Build Coastguard Worker timeout=TEST_TIMEOUT) 449*3ac0a46fSAndroid Build Coastguard Worker self.features = set(output.decode('utf-8').strip().split(',')) 450*3ac0a46fSAndroid Build Coastguard Worker 451*3ac0a46fSAndroid Build Coastguard Worker if 'SKIA' in self.features: 452*3ac0a46fSAndroid Build Coastguard Worker self.rendering_option = 'skia' 453*3ac0a46fSAndroid Build Coastguard Worker else: 454*3ac0a46fSAndroid Build Coastguard Worker self.rendering_option = 'agg' 455*3ac0a46fSAndroid Build Coastguard Worker 456*3ac0a46fSAndroid Build Coastguard Worker if self.options.use_renderer == 'agg': 457*3ac0a46fSAndroid Build Coastguard Worker self.rendering_option = 'agg' 458*3ac0a46fSAndroid Build Coastguard Worker elif self.options.use_renderer == 'gdi': 459*3ac0a46fSAndroid Build Coastguard Worker if 'GDI' not in self.features: 460*3ac0a46fSAndroid Build Coastguard Worker return 'pdfium_test does not support the GDI renderer' 461*3ac0a46fSAndroid Build Coastguard Worker self.rendering_option = 'gdi' 462*3ac0a46fSAndroid Build Coastguard Worker elif self.options.use_renderer == 'skia': 463*3ac0a46fSAndroid Build Coastguard Worker if 'SKIA' not in self.features: 464*3ac0a46fSAndroid Build Coastguard Worker return 'pdfium_test does not support the Skia renderer' 465*3ac0a46fSAndroid Build Coastguard Worker self.rendering_option = 'skia' 466*3ac0a46fSAndroid Build Coastguard Worker 467*3ac0a46fSAndroid Build Coastguard Worker return None 468*3ac0a46fSAndroid Build Coastguard Worker 469*3ac0a46fSAndroid Build Coastguard Worker 470*3ac0a46fSAndroid Build Coastguard Workerclass _PerProcessState: 471*3ac0a46fSAndroid Build Coastguard Worker """State defined per process.""" 472*3ac0a46fSAndroid Build Coastguard Worker 473*3ac0a46fSAndroid Build Coastguard Worker def __init__(self, config): 474*3ac0a46fSAndroid Build Coastguard Worker self.test_dir = config.test_dir 475*3ac0a46fSAndroid Build Coastguard Worker self.test_type = config.test_type 476*3ac0a46fSAndroid Build Coastguard Worker self.delete_output_on_success = config.delete_output_on_success 477*3ac0a46fSAndroid Build Coastguard Worker self.enforce_expected_images = config.enforce_expected_images 478*3ac0a46fSAndroid Build Coastguard Worker self.options = config.options 479*3ac0a46fSAndroid Build Coastguard Worker self.features = config.features 480*3ac0a46fSAndroid Build Coastguard Worker 481*3ac0a46fSAndroid Build Coastguard Worker finder = config.NewFinder() 482*3ac0a46fSAndroid Build Coastguard Worker self.pdfium_test_path = config.GetPdfiumTestPath(finder) 483*3ac0a46fSAndroid Build Coastguard Worker self.fixup_path = finder.ScriptPath('fixup_pdf_template.py') 484*3ac0a46fSAndroid Build Coastguard Worker self.text_diff_path = finder.ScriptPath('text_diff.py') 485*3ac0a46fSAndroid Build Coastguard Worker self.font_dir = os.path.join(finder.TestingDir(), 'resources', 'fonts') 486*3ac0a46fSAndroid Build Coastguard Worker self.third_party_font_dir = finder.ThirdPartyFontsDir() 487*3ac0a46fSAndroid Build Coastguard Worker 488*3ac0a46fSAndroid Build Coastguard Worker self.source_dir = finder.TestingDir() 489*3ac0a46fSAndroid Build Coastguard Worker self.working_dir = finder.WorkingDir(os.path.join('testing', self.test_dir)) 490*3ac0a46fSAndroid Build Coastguard Worker 491*3ac0a46fSAndroid Build Coastguard Worker self.test_suppressor = suppressor.Suppressor( 492*3ac0a46fSAndroid Build Coastguard Worker finder, self.features, self.options.disable_javascript, 493*3ac0a46fSAndroid Build Coastguard Worker self.options.disable_xfa, config.rendering_option) 494*3ac0a46fSAndroid Build Coastguard Worker self.image_differ = pngdiffer.PNGDiffer(finder, 495*3ac0a46fSAndroid Build Coastguard Worker self.options.reverse_byte_order, 496*3ac0a46fSAndroid Build Coastguard Worker config.rendering_option) 497*3ac0a46fSAndroid Build Coastguard Worker 498*3ac0a46fSAndroid Build Coastguard Worker self.process_name = multiprocessing.current_process().name 499*3ac0a46fSAndroid Build Coastguard Worker self.skia_tester = None 500*3ac0a46fSAndroid Build Coastguard Worker 501*3ac0a46fSAndroid Build Coastguard Worker def __getstate__(self): 502*3ac0a46fSAndroid Build Coastguard Worker raise RuntimeError('Cannot pickle per-process state') 503*3ac0a46fSAndroid Build Coastguard Worker 504*3ac0a46fSAndroid Build Coastguard Worker def GetSkiaGoldTester(self): 505*3ac0a46fSAndroid Build Coastguard Worker """Gets the `SkiaGoldTester` singleton for this worker.""" 506*3ac0a46fSAndroid Build Coastguard Worker if not self.skia_tester: 507*3ac0a46fSAndroid Build Coastguard Worker self.skia_tester = skia_gold.SkiaGoldTester( 508*3ac0a46fSAndroid Build Coastguard Worker source_type=self.test_type, 509*3ac0a46fSAndroid Build Coastguard Worker skia_gold_args=self.options, 510*3ac0a46fSAndroid Build Coastguard Worker process_name=self.process_name) 511*3ac0a46fSAndroid Build Coastguard Worker return self.skia_tester 512*3ac0a46fSAndroid Build Coastguard Worker 513*3ac0a46fSAndroid Build Coastguard Worker 514*3ac0a46fSAndroid Build Coastguard Workerclass _TestCaseRunner: 515*3ac0a46fSAndroid Build Coastguard Worker """Runner for a single test case.""" 516*3ac0a46fSAndroid Build Coastguard Worker 517*3ac0a46fSAndroid Build Coastguard Worker def __init__(self, test_case): 518*3ac0a46fSAndroid Build Coastguard Worker self.test_case = test_case 519*3ac0a46fSAndroid Build Coastguard Worker self.test_result = None 520*3ac0a46fSAndroid Build Coastguard Worker self.duration_start = 0 521*3ac0a46fSAndroid Build Coastguard Worker 522*3ac0a46fSAndroid Build Coastguard Worker self.source_dir, self.input_filename = os.path.split( 523*3ac0a46fSAndroid Build Coastguard Worker self.test_case.input_path) 524*3ac0a46fSAndroid Build Coastguard Worker self.pdf_path = os.path.join(self.working_dir, f'{self.test_id}.pdf') 525*3ac0a46fSAndroid Build Coastguard Worker self.actual_images = None 526*3ac0a46fSAndroid Build Coastguard Worker 527*3ac0a46fSAndroid Build Coastguard Worker def __enter__(self): 528*3ac0a46fSAndroid Build Coastguard Worker self.duration_start = time.perf_counter_ns() 529*3ac0a46fSAndroid Build Coastguard Worker return self 530*3ac0a46fSAndroid Build Coastguard Worker 531*3ac0a46fSAndroid Build Coastguard Worker def __exit__(self, exc_type, exc_value, traceback): 532*3ac0a46fSAndroid Build Coastguard Worker if not self.test_result: 533*3ac0a46fSAndroid Build Coastguard Worker self.test_result = self.test_case.NewResult( 534*3ac0a46fSAndroid Build Coastguard Worker result_types.UNKNOWN, reason='No test result recorded') 535*3ac0a46fSAndroid Build Coastguard Worker duration = time.perf_counter_ns() - self.duration_start 536*3ac0a46fSAndroid Build Coastguard Worker self.test_result.duration_milliseconds = duration * 1e-6 537*3ac0a46fSAndroid Build Coastguard Worker 538*3ac0a46fSAndroid Build Coastguard Worker @property 539*3ac0a46fSAndroid Build Coastguard Worker def options(self): 540*3ac0a46fSAndroid Build Coastguard Worker return _per_process_state.options 541*3ac0a46fSAndroid Build Coastguard Worker 542*3ac0a46fSAndroid Build Coastguard Worker @property 543*3ac0a46fSAndroid Build Coastguard Worker def test_id(self): 544*3ac0a46fSAndroid Build Coastguard Worker return self.test_case.test_id 545*3ac0a46fSAndroid Build Coastguard Worker 546*3ac0a46fSAndroid Build Coastguard Worker @property 547*3ac0a46fSAndroid Build Coastguard Worker def working_dir(self): 548*3ac0a46fSAndroid Build Coastguard Worker return _per_process_state.working_dir 549*3ac0a46fSAndroid Build Coastguard Worker 550*3ac0a46fSAndroid Build Coastguard Worker def IsResultSuppressed(self): 551*3ac0a46fSAndroid Build Coastguard Worker return _per_process_state.test_suppressor.IsResultSuppressed( 552*3ac0a46fSAndroid Build Coastguard Worker self.input_filename) 553*3ac0a46fSAndroid Build Coastguard Worker 554*3ac0a46fSAndroid Build Coastguard Worker def IsImageDiffSuppressed(self): 555*3ac0a46fSAndroid Build Coastguard Worker return _per_process_state.test_suppressor.IsImageDiffSuppressed( 556*3ac0a46fSAndroid Build Coastguard Worker self.input_filename) 557*3ac0a46fSAndroid Build Coastguard Worker 558*3ac0a46fSAndroid Build Coastguard Worker def GetImageMatchingAlgorithm(self): 559*3ac0a46fSAndroid Build Coastguard Worker return _per_process_state.test_suppressor.GetImageMatchingAlgorithm( 560*3ac0a46fSAndroid Build Coastguard Worker self.input_filename) 561*3ac0a46fSAndroid Build Coastguard Worker 562*3ac0a46fSAndroid Build Coastguard Worker def RunCommand(self, command, stdout=None): 563*3ac0a46fSAndroid Build Coastguard Worker """Runs a test command. 564*3ac0a46fSAndroid Build Coastguard Worker 565*3ac0a46fSAndroid Build Coastguard Worker Args: 566*3ac0a46fSAndroid Build Coastguard Worker command: The list of command arguments. 567*3ac0a46fSAndroid Build Coastguard Worker stdout: Optional `file`-like object to send standard output. 568*3ac0a46fSAndroid Build Coastguard Worker 569*3ac0a46fSAndroid Build Coastguard Worker Returns: 570*3ac0a46fSAndroid Build Coastguard Worker The test result. 571*3ac0a46fSAndroid Build Coastguard Worker """ 572*3ac0a46fSAndroid Build Coastguard Worker 573*3ac0a46fSAndroid Build Coastguard Worker # Standard output and error are directed to the test log. If `stdout` was 574*3ac0a46fSAndroid Build Coastguard Worker # provided, redirect standard output to it instead. 575*3ac0a46fSAndroid Build Coastguard Worker if stdout: 576*3ac0a46fSAndroid Build Coastguard Worker assert stdout != subprocess.PIPE 577*3ac0a46fSAndroid Build Coastguard Worker try: 578*3ac0a46fSAndroid Build Coastguard Worker stdout.fileno() 579*3ac0a46fSAndroid Build Coastguard Worker except OSError: 580*3ac0a46fSAndroid Build Coastguard Worker # `stdout` doesn't have a file descriptor, so it can't be passed to 581*3ac0a46fSAndroid Build Coastguard Worker # `subprocess.run()` directly. 582*3ac0a46fSAndroid Build Coastguard Worker original_stdout = stdout 583*3ac0a46fSAndroid Build Coastguard Worker stdout = subprocess.PIPE 584*3ac0a46fSAndroid Build Coastguard Worker stderr = subprocess.PIPE 585*3ac0a46fSAndroid Build Coastguard Worker else: 586*3ac0a46fSAndroid Build Coastguard Worker stdout = subprocess.PIPE 587*3ac0a46fSAndroid Build Coastguard Worker stderr = subprocess.STDOUT 588*3ac0a46fSAndroid Build Coastguard Worker 589*3ac0a46fSAndroid Build Coastguard Worker test_result = self.test_case.NewResult(result_types.PASS) 590*3ac0a46fSAndroid Build Coastguard Worker try: 591*3ac0a46fSAndroid Build Coastguard Worker run_result = subprocess.run( 592*3ac0a46fSAndroid Build Coastguard Worker command, 593*3ac0a46fSAndroid Build Coastguard Worker stdout=stdout, 594*3ac0a46fSAndroid Build Coastguard Worker stderr=stderr, 595*3ac0a46fSAndroid Build Coastguard Worker timeout=TEST_TIMEOUT, 596*3ac0a46fSAndroid Build Coastguard Worker check=False) 597*3ac0a46fSAndroid Build Coastguard Worker if run_result.returncode != 0: 598*3ac0a46fSAndroid Build Coastguard Worker test_result.status = result_types.FAIL 599*3ac0a46fSAndroid Build Coastguard Worker test_result.reason = 'Command {} exited with code {}'.format( 600*3ac0a46fSAndroid Build Coastguard Worker run_result.args, run_result.returncode) 601*3ac0a46fSAndroid Build Coastguard Worker except subprocess.TimeoutExpired as timeout_expired: 602*3ac0a46fSAndroid Build Coastguard Worker run_result = timeout_expired 603*3ac0a46fSAndroid Build Coastguard Worker test_result.status = result_types.TIMEOUT 604*3ac0a46fSAndroid Build Coastguard Worker test_result.reason = 'Command {} timed out'.format(run_result.cmd) 605*3ac0a46fSAndroid Build Coastguard Worker 606*3ac0a46fSAndroid Build Coastguard Worker if stdout == subprocess.PIPE and stderr == subprocess.PIPE: 607*3ac0a46fSAndroid Build Coastguard Worker # Copy captured standard output, if any, to the original `stdout`. 608*3ac0a46fSAndroid Build Coastguard Worker if run_result.stdout: 609*3ac0a46fSAndroid Build Coastguard Worker original_stdout.write(run_result.stdout) 610*3ac0a46fSAndroid Build Coastguard Worker 611*3ac0a46fSAndroid Build Coastguard Worker if not test_result.IsPass(): 612*3ac0a46fSAndroid Build Coastguard Worker # On failure, report captured output to the test log. 613*3ac0a46fSAndroid Build Coastguard Worker if stderr == subprocess.STDOUT: 614*3ac0a46fSAndroid Build Coastguard Worker test_result.log = run_result.stdout 615*3ac0a46fSAndroid Build Coastguard Worker else: 616*3ac0a46fSAndroid Build Coastguard Worker test_result.log = run_result.stderr 617*3ac0a46fSAndroid Build Coastguard Worker test_result.log = test_result.log.decode(errors='backslashreplace') 618*3ac0a46fSAndroid Build Coastguard Worker return test_result 619*3ac0a46fSAndroid Build Coastguard Worker 620*3ac0a46fSAndroid Build Coastguard Worker def GenerateAndTest(self, test_function): 621*3ac0a46fSAndroid Build Coastguard Worker """Generate test input and run pdfium_test.""" 622*3ac0a46fSAndroid Build Coastguard Worker test_result = self.Generate() 623*3ac0a46fSAndroid Build Coastguard Worker if not test_result.IsPass(): 624*3ac0a46fSAndroid Build Coastguard Worker return test_result 625*3ac0a46fSAndroid Build Coastguard Worker 626*3ac0a46fSAndroid Build Coastguard Worker return test_function() 627*3ac0a46fSAndroid Build Coastguard Worker 628*3ac0a46fSAndroid Build Coastguard Worker def _RegenerateIfNeeded(self): 629*3ac0a46fSAndroid Build Coastguard Worker if not self.options.regenerate_expected: 630*3ac0a46fSAndroid Build Coastguard Worker return 631*3ac0a46fSAndroid Build Coastguard Worker if self.IsResultSuppressed() or self.IsImageDiffSuppressed(): 632*3ac0a46fSAndroid Build Coastguard Worker return 633*3ac0a46fSAndroid Build Coastguard Worker _per_process_state.image_differ.Regenerate( 634*3ac0a46fSAndroid Build Coastguard Worker self.input_filename, 635*3ac0a46fSAndroid Build Coastguard Worker self.source_dir, 636*3ac0a46fSAndroid Build Coastguard Worker self.working_dir, 637*3ac0a46fSAndroid Build Coastguard Worker image_matching_algorithm=self.GetImageMatchingAlgorithm()) 638*3ac0a46fSAndroid Build Coastguard Worker 639*3ac0a46fSAndroid Build Coastguard Worker def Generate(self): 640*3ac0a46fSAndroid Build Coastguard Worker input_event_path = os.path.join(self.source_dir, f'{self.test_id}.evt') 641*3ac0a46fSAndroid Build Coastguard Worker if os.path.exists(input_event_path): 642*3ac0a46fSAndroid Build Coastguard Worker output_event_path = f'{os.path.splitext(self.pdf_path)[0]}.evt' 643*3ac0a46fSAndroid Build Coastguard Worker shutil.copyfile(input_event_path, output_event_path) 644*3ac0a46fSAndroid Build Coastguard Worker 645*3ac0a46fSAndroid Build Coastguard Worker template_path = os.path.join(self.source_dir, f'{self.test_id}.in') 646*3ac0a46fSAndroid Build Coastguard Worker if not os.path.exists(template_path): 647*3ac0a46fSAndroid Build Coastguard Worker if os.path.exists(self.test_case.input_path): 648*3ac0a46fSAndroid Build Coastguard Worker shutil.copyfile(self.test_case.input_path, self.pdf_path) 649*3ac0a46fSAndroid Build Coastguard Worker return self.test_case.NewResult(result_types.PASS) 650*3ac0a46fSAndroid Build Coastguard Worker 651*3ac0a46fSAndroid Build Coastguard Worker return self.RunCommand([ 652*3ac0a46fSAndroid Build Coastguard Worker sys.executable, _per_process_state.fixup_path, 653*3ac0a46fSAndroid Build Coastguard Worker f'--output-dir={self.working_dir}', template_path 654*3ac0a46fSAndroid Build Coastguard Worker ]) 655*3ac0a46fSAndroid Build Coastguard Worker 656*3ac0a46fSAndroid Build Coastguard Worker def TestText(self): 657*3ac0a46fSAndroid Build Coastguard Worker txt_path = os.path.join(self.working_dir, f'{self.test_id}.txt') 658*3ac0a46fSAndroid Build Coastguard Worker with open(txt_path, 'w') as outfile: 659*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run = [ 660*3ac0a46fSAndroid Build Coastguard Worker _per_process_state.pdfium_test_path, '--send-events', 661*3ac0a46fSAndroid Build Coastguard Worker f'--time={TEST_SEED_TIME}' 662*3ac0a46fSAndroid Build Coastguard Worker ] 663*3ac0a46fSAndroid Build Coastguard Worker 664*3ac0a46fSAndroid Build Coastguard Worker if self.options.disable_javascript: 665*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append('--disable-javascript') 666*3ac0a46fSAndroid Build Coastguard Worker 667*3ac0a46fSAndroid Build Coastguard Worker if self.options.disable_xfa: 668*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append('--disable-xfa') 669*3ac0a46fSAndroid Build Coastguard Worker 670*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append(self.pdf_path) 671*3ac0a46fSAndroid Build Coastguard Worker test_result = self.RunCommand(cmd_to_run, stdout=outfile) 672*3ac0a46fSAndroid Build Coastguard Worker if not test_result.IsPass(): 673*3ac0a46fSAndroid Build Coastguard Worker return test_result 674*3ac0a46fSAndroid Build Coastguard Worker 675*3ac0a46fSAndroid Build Coastguard Worker # If the expected file does not exist, the output is expected to be empty. 676*3ac0a46fSAndroid Build Coastguard Worker expected_txt_path = os.path.join(self.source_dir, 677*3ac0a46fSAndroid Build Coastguard Worker f'{self.test_id}_expected.txt') 678*3ac0a46fSAndroid Build Coastguard Worker if not os.path.exists(expected_txt_path): 679*3ac0a46fSAndroid Build Coastguard Worker return self._VerifyEmptyText(txt_path) 680*3ac0a46fSAndroid Build Coastguard Worker 681*3ac0a46fSAndroid Build Coastguard Worker # If JavaScript is disabled, the output should be empty. 682*3ac0a46fSAndroid Build Coastguard Worker # However, if the test is suppressed and JavaScript is disabled, do not 683*3ac0a46fSAndroid Build Coastguard Worker # verify that the text is empty so the suppressed test does not surprise. 684*3ac0a46fSAndroid Build Coastguard Worker if self.options.disable_javascript and not self.IsResultSuppressed(): 685*3ac0a46fSAndroid Build Coastguard Worker return self._VerifyEmptyText(txt_path) 686*3ac0a46fSAndroid Build Coastguard Worker 687*3ac0a46fSAndroid Build Coastguard Worker return self.RunCommand([ 688*3ac0a46fSAndroid Build Coastguard Worker sys.executable, _per_process_state.text_diff_path, expected_txt_path, 689*3ac0a46fSAndroid Build Coastguard Worker txt_path 690*3ac0a46fSAndroid Build Coastguard Worker ]) 691*3ac0a46fSAndroid Build Coastguard Worker 692*3ac0a46fSAndroid Build Coastguard Worker def _VerifyEmptyText(self, txt_path): 693*3ac0a46fSAndroid Build Coastguard Worker with open(txt_path, "rb") as txt_file: 694*3ac0a46fSAndroid Build Coastguard Worker txt_data = txt_file.read() 695*3ac0a46fSAndroid Build Coastguard Worker 696*3ac0a46fSAndroid Build Coastguard Worker if txt_data: 697*3ac0a46fSAndroid Build Coastguard Worker return self.test_case.NewResult( 698*3ac0a46fSAndroid Build Coastguard Worker result_types.FAIL, 699*3ac0a46fSAndroid Build Coastguard Worker log=txt_data.decode(errors='backslashreplace'), 700*3ac0a46fSAndroid Build Coastguard Worker reason=f'{txt_path} should be empty') 701*3ac0a46fSAndroid Build Coastguard Worker 702*3ac0a46fSAndroid Build Coastguard Worker return self.test_case.NewResult(result_types.PASS) 703*3ac0a46fSAndroid Build Coastguard Worker 704*3ac0a46fSAndroid Build Coastguard Worker # TODO(crbug.com/pdfium/1656): Remove when ready to fully switch over to 705*3ac0a46fSAndroid Build Coastguard Worker # Skia Gold 706*3ac0a46fSAndroid Build Coastguard Worker def TestPixel(self): 707*3ac0a46fSAndroid Build Coastguard Worker # Remove any existing generated images from previous runs. 708*3ac0a46fSAndroid Build Coastguard Worker self.actual_images = _per_process_state.image_differ.GetActualFiles( 709*3ac0a46fSAndroid Build Coastguard Worker self.input_filename, self.source_dir, self.working_dir) 710*3ac0a46fSAndroid Build Coastguard Worker self._CleanupPixelTest() 711*3ac0a46fSAndroid Build Coastguard Worker 712*3ac0a46fSAndroid Build Coastguard Worker # Generate images. 713*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run = [ 714*3ac0a46fSAndroid Build Coastguard Worker _per_process_state.pdfium_test_path, '--send-events', '--png', '--md5', 715*3ac0a46fSAndroid Build Coastguard Worker f'--time={TEST_SEED_TIME}' 716*3ac0a46fSAndroid Build Coastguard Worker ] 717*3ac0a46fSAndroid Build Coastguard Worker 718*3ac0a46fSAndroid Build Coastguard Worker if 'use_ahem' in self.source_dir or 'use_symbolneu' in self.source_dir: 719*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append(f'--font-dir={_per_process_state.font_dir}') 720*3ac0a46fSAndroid Build Coastguard Worker else: 721*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append(f'--font-dir={_per_process_state.third_party_font_dir}') 722*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append('--croscore-font-names') 723*3ac0a46fSAndroid Build Coastguard Worker 724*3ac0a46fSAndroid Build Coastguard Worker if self.options.disable_javascript: 725*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append('--disable-javascript') 726*3ac0a46fSAndroid Build Coastguard Worker 727*3ac0a46fSAndroid Build Coastguard Worker if self.options.disable_xfa: 728*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append('--disable-xfa') 729*3ac0a46fSAndroid Build Coastguard Worker 730*3ac0a46fSAndroid Build Coastguard Worker if self.options.render_oneshot: 731*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append('--render-oneshot') 732*3ac0a46fSAndroid Build Coastguard Worker 733*3ac0a46fSAndroid Build Coastguard Worker if self.options.reverse_byte_order: 734*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append('--reverse-byte-order') 735*3ac0a46fSAndroid Build Coastguard Worker 736*3ac0a46fSAndroid Build Coastguard Worker if self.options.use_renderer: 737*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append(f'--use-renderer={self.options.use_renderer}') 738*3ac0a46fSAndroid Build Coastguard Worker 739*3ac0a46fSAndroid Build Coastguard Worker cmd_to_run.append(self.pdf_path) 740*3ac0a46fSAndroid Build Coastguard Worker 741*3ac0a46fSAndroid Build Coastguard Worker with BytesIO() as command_output: 742*3ac0a46fSAndroid Build Coastguard Worker test_result = self.RunCommand(cmd_to_run, stdout=command_output) 743*3ac0a46fSAndroid Build Coastguard Worker if not test_result.IsPass(): 744*3ac0a46fSAndroid Build Coastguard Worker return test_result 745*3ac0a46fSAndroid Build Coastguard Worker 746*3ac0a46fSAndroid Build Coastguard Worker test_result.image_artifacts = [] 747*3ac0a46fSAndroid Build Coastguard Worker for line in command_output.getvalue().splitlines(): 748*3ac0a46fSAndroid Build Coastguard Worker # Expect this format: MD5:<path to image file>:<hexadecimal MD5 hash> 749*3ac0a46fSAndroid Build Coastguard Worker line = bytes.decode(line).strip() 750*3ac0a46fSAndroid Build Coastguard Worker if line.startswith('MD5:'): 751*3ac0a46fSAndroid Build Coastguard Worker image_path, md5_hash = line[4:].rsplit(':', 1) 752*3ac0a46fSAndroid Build Coastguard Worker test_result.image_artifacts.append( 753*3ac0a46fSAndroid Build Coastguard Worker self._NewImageArtifact( 754*3ac0a46fSAndroid Build Coastguard Worker image_path=image_path.strip(), md5_hash=md5_hash.strip())) 755*3ac0a46fSAndroid Build Coastguard Worker 756*3ac0a46fSAndroid Build Coastguard Worker if self.actual_images: 757*3ac0a46fSAndroid Build Coastguard Worker image_diffs = _per_process_state.image_differ.ComputeDifferences( 758*3ac0a46fSAndroid Build Coastguard Worker self.input_filename, 759*3ac0a46fSAndroid Build Coastguard Worker self.source_dir, 760*3ac0a46fSAndroid Build Coastguard Worker self.working_dir, 761*3ac0a46fSAndroid Build Coastguard Worker image_matching_algorithm=self.GetImageMatchingAlgorithm()) 762*3ac0a46fSAndroid Build Coastguard Worker if image_diffs: 763*3ac0a46fSAndroid Build Coastguard Worker test_result.status = result_types.FAIL 764*3ac0a46fSAndroid Build Coastguard Worker test_result.reason = 'Images differ' 765*3ac0a46fSAndroid Build Coastguard Worker 766*3ac0a46fSAndroid Build Coastguard Worker # Merge image diffs into test result. 767*3ac0a46fSAndroid Build Coastguard Worker diff_map = {} 768*3ac0a46fSAndroid Build Coastguard Worker diff_log = [] 769*3ac0a46fSAndroid Build Coastguard Worker for diff in image_diffs: 770*3ac0a46fSAndroid Build Coastguard Worker diff_map[diff.actual_path] = diff 771*3ac0a46fSAndroid Build Coastguard Worker diff_log.append(f'{os.path.basename(diff.actual_path)} vs. ') 772*3ac0a46fSAndroid Build Coastguard Worker if diff.expected_path: 773*3ac0a46fSAndroid Build Coastguard Worker diff_log.append(f'{os.path.basename(diff.expected_path)}\n') 774*3ac0a46fSAndroid Build Coastguard Worker else: 775*3ac0a46fSAndroid Build Coastguard Worker diff_log.append('missing expected file\n') 776*3ac0a46fSAndroid Build Coastguard Worker 777*3ac0a46fSAndroid Build Coastguard Worker for artifact in test_result.image_artifacts: 778*3ac0a46fSAndroid Build Coastguard Worker artifact.image_diff = diff_map.get(artifact.image_path) 779*3ac0a46fSAndroid Build Coastguard Worker test_result.log = ''.join(diff_log) 780*3ac0a46fSAndroid Build Coastguard Worker 781*3ac0a46fSAndroid Build Coastguard Worker elif _per_process_state.enforce_expected_images: 782*3ac0a46fSAndroid Build Coastguard Worker if not self.IsImageDiffSuppressed(): 783*3ac0a46fSAndroid Build Coastguard Worker test_result.status = result_types.FAIL 784*3ac0a46fSAndroid Build Coastguard Worker test_result.reason = 'Missing expected images' 785*3ac0a46fSAndroid Build Coastguard Worker 786*3ac0a46fSAndroid Build Coastguard Worker if not test_result.IsPass(): 787*3ac0a46fSAndroid Build Coastguard Worker self._RegenerateIfNeeded() 788*3ac0a46fSAndroid Build Coastguard Worker return test_result 789*3ac0a46fSAndroid Build Coastguard Worker 790*3ac0a46fSAndroid Build Coastguard Worker if _per_process_state.delete_output_on_success: 791*3ac0a46fSAndroid Build Coastguard Worker self._CleanupPixelTest() 792*3ac0a46fSAndroid Build Coastguard Worker return test_result 793*3ac0a46fSAndroid Build Coastguard Worker 794*3ac0a46fSAndroid Build Coastguard Worker def _NewImageArtifact(self, *, image_path, md5_hash): 795*3ac0a46fSAndroid Build Coastguard Worker artifact = ImageArtifact(image_path=image_path, md5_hash=md5_hash) 796*3ac0a46fSAndroid Build Coastguard Worker 797*3ac0a46fSAndroid Build Coastguard Worker if self.options.run_skia_gold: 798*3ac0a46fSAndroid Build Coastguard Worker if _per_process_state.GetSkiaGoldTester().UploadTestResultToSkiaGold( 799*3ac0a46fSAndroid Build Coastguard Worker artifact.GetSkiaGoldId(), artifact.image_path): 800*3ac0a46fSAndroid Build Coastguard Worker artifact.skia_gold_status = result_types.PASS 801*3ac0a46fSAndroid Build Coastguard Worker else: 802*3ac0a46fSAndroid Build Coastguard Worker artifact.skia_gold_status = result_types.FAIL 803*3ac0a46fSAndroid Build Coastguard Worker 804*3ac0a46fSAndroid Build Coastguard Worker return artifact 805*3ac0a46fSAndroid Build Coastguard Worker 806*3ac0a46fSAndroid Build Coastguard Worker def _CleanupPixelTest(self): 807*3ac0a46fSAndroid Build Coastguard Worker for image_file in self.actual_images: 808*3ac0a46fSAndroid Build Coastguard Worker if os.path.exists(image_file): 809*3ac0a46fSAndroid Build Coastguard Worker os.remove(image_file) 810*3ac0a46fSAndroid Build Coastguard Worker 811*3ac0a46fSAndroid Build Coastguard Worker 812*3ac0a46fSAndroid Build Coastguard Worker@dataclass 813*3ac0a46fSAndroid Build Coastguard Workerclass TestCase: 814*3ac0a46fSAndroid Build Coastguard Worker """Description of a test case to run. 815*3ac0a46fSAndroid Build Coastguard Worker 816*3ac0a46fSAndroid Build Coastguard Worker Attributes: 817*3ac0a46fSAndroid Build Coastguard Worker test_id: A unique identifier for the test. 818*3ac0a46fSAndroid Build Coastguard Worker input_path: The absolute path to the test file. 819*3ac0a46fSAndroid Build Coastguard Worker """ 820*3ac0a46fSAndroid Build Coastguard Worker test_id: str 821*3ac0a46fSAndroid Build Coastguard Worker input_path: str 822*3ac0a46fSAndroid Build Coastguard Worker 823*3ac0a46fSAndroid Build Coastguard Worker def NewResult(self, status, **kwargs): 824*3ac0a46fSAndroid Build Coastguard Worker """Derives a new test result corresponding to this test case.""" 825*3ac0a46fSAndroid Build Coastguard Worker return TestResult(test_id=self.test_id, status=status, **kwargs) 826*3ac0a46fSAndroid Build Coastguard Worker 827*3ac0a46fSAndroid Build Coastguard Worker 828*3ac0a46fSAndroid Build Coastguard Worker@dataclass 829*3ac0a46fSAndroid Build Coastguard Workerclass TestResult: 830*3ac0a46fSAndroid Build Coastguard Worker """Results from running a test case. 831*3ac0a46fSAndroid Build Coastguard Worker 832*3ac0a46fSAndroid Build Coastguard Worker Attributes: 833*3ac0a46fSAndroid Build Coastguard Worker test_id: The corresponding test case ID. 834*3ac0a46fSAndroid Build Coastguard Worker status: The overall `result_types` status. 835*3ac0a46fSAndroid Build Coastguard Worker duration_milliseconds: Test time in milliseconds. 836*3ac0a46fSAndroid Build Coastguard Worker log: Optional log of the test's output. 837*3ac0a46fSAndroid Build Coastguard Worker image_artfacts: Optional list of image artifacts. 838*3ac0a46fSAndroid Build Coastguard Worker reason: Optional reason why the test failed. 839*3ac0a46fSAndroid Build Coastguard Worker """ 840*3ac0a46fSAndroid Build Coastguard Worker test_id: str 841*3ac0a46fSAndroid Build Coastguard Worker status: str 842*3ac0a46fSAndroid Build Coastguard Worker duration_milliseconds: float = None 843*3ac0a46fSAndroid Build Coastguard Worker log: str = None 844*3ac0a46fSAndroid Build Coastguard Worker image_artifacts: list = field(default_factory=list) 845*3ac0a46fSAndroid Build Coastguard Worker reason: str = None 846*3ac0a46fSAndroid Build Coastguard Worker 847*3ac0a46fSAndroid Build Coastguard Worker def IsPass(self): 848*3ac0a46fSAndroid Build Coastguard Worker """Whether the test passed.""" 849*3ac0a46fSAndroid Build Coastguard Worker return self.status == result_types.PASS 850*3ac0a46fSAndroid Build Coastguard Worker 851*3ac0a46fSAndroid Build Coastguard Worker 852*3ac0a46fSAndroid Build Coastguard Worker@dataclass 853*3ac0a46fSAndroid Build Coastguard Workerclass ImageArtifact: 854*3ac0a46fSAndroid Build Coastguard Worker """Image artifact for a test result. 855*3ac0a46fSAndroid Build Coastguard Worker 856*3ac0a46fSAndroid Build Coastguard Worker Attributes: 857*3ac0a46fSAndroid Build Coastguard Worker image_path: The absolute path to the image file. 858*3ac0a46fSAndroid Build Coastguard Worker md5_hash: The MD5 hash of the pixel buffer. 859*3ac0a46fSAndroid Build Coastguard Worker skia_gold_status: Optional Skia Gold status. 860*3ac0a46fSAndroid Build Coastguard Worker image_diff: Optional image diff. 861*3ac0a46fSAndroid Build Coastguard Worker """ 862*3ac0a46fSAndroid Build Coastguard Worker image_path: str 863*3ac0a46fSAndroid Build Coastguard Worker md5_hash: str 864*3ac0a46fSAndroid Build Coastguard Worker skia_gold_status: str = None 865*3ac0a46fSAndroid Build Coastguard Worker image_diff: pngdiffer.ImageDiff = None 866*3ac0a46fSAndroid Build Coastguard Worker 867*3ac0a46fSAndroid Build Coastguard Worker def GetSkiaGoldId(self): 868*3ac0a46fSAndroid Build Coastguard Worker # The output filename without image extension becomes the test ID. For 869*3ac0a46fSAndroid Build Coastguard Worker # example, "/path/to/.../testing/corpus/example_005.pdf.0.png" becomes 870*3ac0a46fSAndroid Build Coastguard Worker # "example_005.pdf.0". 871*3ac0a46fSAndroid Build Coastguard Worker return _GetTestId(os.path.basename(self.image_path)) 872*3ac0a46fSAndroid Build Coastguard Worker 873*3ac0a46fSAndroid Build Coastguard Worker def GetDiffStatus(self): 874*3ac0a46fSAndroid Build Coastguard Worker return result_types.FAIL if self.image_diff else result_types.PASS 875*3ac0a46fSAndroid Build Coastguard Worker 876*3ac0a46fSAndroid Build Coastguard Worker def GetDiffReason(self): 877*3ac0a46fSAndroid Build Coastguard Worker return self.image_diff.reason if self.image_diff else None 878*3ac0a46fSAndroid Build Coastguard Worker 879*3ac0a46fSAndroid Build Coastguard Worker def GetDiffArtifacts(self): 880*3ac0a46fSAndroid Build Coastguard Worker if not self.image_diff: 881*3ac0a46fSAndroid Build Coastguard Worker return None 882*3ac0a46fSAndroid Build Coastguard Worker if not self.image_diff.expected_path or not self.image_diff.diff_path: 883*3ac0a46fSAndroid Build Coastguard Worker return None 884*3ac0a46fSAndroid Build Coastguard Worker return { 885*3ac0a46fSAndroid Build Coastguard Worker 'actual_image': 886*3ac0a46fSAndroid Build Coastguard Worker _GetArtifactFromFilePath(self.image_path), 887*3ac0a46fSAndroid Build Coastguard Worker 'expected_image': 888*3ac0a46fSAndroid Build Coastguard Worker _GetArtifactFromFilePath(self.image_diff.expected_path), 889*3ac0a46fSAndroid Build Coastguard Worker 'image_diff': 890*3ac0a46fSAndroid Build Coastguard Worker _GetArtifactFromFilePath(self.image_diff.diff_path) 891*3ac0a46fSAndroid Build Coastguard Worker } 892*3ac0a46fSAndroid Build Coastguard Worker 893*3ac0a46fSAndroid Build Coastguard Worker 894*3ac0a46fSAndroid Build Coastguard Workerclass TestCaseManager: 895*3ac0a46fSAndroid Build Coastguard Worker """Manages a collection of test cases.""" 896*3ac0a46fSAndroid Build Coastguard Worker 897*3ac0a46fSAndroid Build Coastguard Worker def __init__(self): 898*3ac0a46fSAndroid Build Coastguard Worker self.test_cases = {} 899*3ac0a46fSAndroid Build Coastguard Worker 900*3ac0a46fSAndroid Build Coastguard Worker def __len__(self): 901*3ac0a46fSAndroid Build Coastguard Worker return len(self.test_cases) 902*3ac0a46fSAndroid Build Coastguard Worker 903*3ac0a46fSAndroid Build Coastguard Worker def __iter__(self): 904*3ac0a46fSAndroid Build Coastguard Worker return iter(self.test_cases.values()) 905*3ac0a46fSAndroid Build Coastguard Worker 906*3ac0a46fSAndroid Build Coastguard Worker def NewTestCase(self, input_path, **kwargs): 907*3ac0a46fSAndroid Build Coastguard Worker """Creates and registers a new test case.""" 908*3ac0a46fSAndroid Build Coastguard Worker input_basename = os.path.basename(input_path) 909*3ac0a46fSAndroid Build Coastguard Worker test_id = _GetTestId(input_basename) 910*3ac0a46fSAndroid Build Coastguard Worker if test_id in self.test_cases: 911*3ac0a46fSAndroid Build Coastguard Worker raise ValueError( 912*3ac0a46fSAndroid Build Coastguard Worker f'Test ID "{test_id}" derived from "{input_basename}" must be unique') 913*3ac0a46fSAndroid Build Coastguard Worker 914*3ac0a46fSAndroid Build Coastguard Worker test_case = TestCase(test_id=test_id, input_path=input_path, **kwargs) 915*3ac0a46fSAndroid Build Coastguard Worker self.test_cases[test_id] = test_case 916*3ac0a46fSAndroid Build Coastguard Worker return test_case 917*3ac0a46fSAndroid Build Coastguard Worker 918*3ac0a46fSAndroid Build Coastguard Worker def GetTestCase(self, test_id): 919*3ac0a46fSAndroid Build Coastguard Worker """Looks up a test case previously registered by `NewTestCase()`.""" 920*3ac0a46fSAndroid Build Coastguard Worker return self.test_cases[test_id] 921*3ac0a46fSAndroid Build Coastguard Worker 922*3ac0a46fSAndroid Build Coastguard Worker 923*3ac0a46fSAndroid Build Coastguard Workerdef _GetTestId(input_basename): 924*3ac0a46fSAndroid Build Coastguard Worker """Constructs a test ID by stripping the last extension from the basename.""" 925*3ac0a46fSAndroid Build Coastguard Worker return os.path.splitext(input_basename)[0] 926*3ac0a46fSAndroid Build Coastguard Worker 927*3ac0a46fSAndroid Build Coastguard Worker 928*3ac0a46fSAndroid Build Coastguard Workerdef _GetArtifactFromFilePath(file_path): 929*3ac0a46fSAndroid Build Coastguard Worker """Constructs a ResultSink artifact from a file path.""" 930*3ac0a46fSAndroid Build Coastguard Worker return {'filePath': file_path} 931