def run_reduction_part( reduction_part_output_dir: Path, source_dir_to_reduce: Path, preserve_semantics: bool, binary_manager: binaries_util.BinaryManager, settings: Settings, ) -> Path: test = test_util.metadata_read_from_source_dir(source_dir_to_reduce) if not test.device or not test.device.name: raise AssertionError( f"Cannot reduce {str(source_dir_to_reduce)}; " f"device must be specified in {str(test_util.get_metadata_path_from_source_dir(source_dir_to_reduce))}" ) if not test.crash_signature: raise AssertionError( f"Cannot reduce {str(source_dir_to_reduce)} because there is no crash string specified." ) shader_jobs = tool.get_shader_jobs(source_dir_to_reduce) # TODO: if needed, this could become a parameter to this function. name_of_shader_to_reduce = shader_jobs[0].name if len(shader_jobs) > 1: check( len(shader_jobs) == 2 and shader_jobs[1].name == test_util.VARIANT_DIR, AssertionError( "Can only reduce tests with shader jobs reference and variant, or just variant." ), ) name_of_shader_to_reduce = shader_jobs[1].name reduction_work_variant_dir = run_glsl_reduce( source_dir=source_dir_to_reduce, name_of_shader_to_reduce=name_of_shader_to_reduce, output_dir=test_util.get_reduction_work_directory( reduction_part_output_dir, name_of_shader_to_reduce), binary_manager=binary_manager, preserve_semantics=preserve_semantics, extra_args=list(settings.extra_graphics_fuzz_reduce_args) if settings.extra_graphics_fuzz_reduce_args else None, ) final_reduced_shader_job_path = get_final_reduced_shader_job_path( reduction_work_variant_dir) check( final_reduced_shader_job_path.exists(), ReductionFailedError("Reduction failed.", reduction_work_variant_dir), ) # Finally, create the output source_dir. util.copy_dir(source_dir_to_reduce, test_util.get_source_dir(reduction_part_output_dir)) shader_job_util.copy( final_reduced_shader_job_path, test_util.get_shader_job_path(reduction_part_output_dir, name_of_shader_to_reduce), ) if not settings.keep_reduction_work: shutil.rmtree(reduction_work_variant_dir) return test_util.get_source_dir(reduction_part_output_dir)
def run_reduction( test_dir_reduction_output: Path, test_dir_to_reduce: Path, preserve_semantics: bool, binary_manager: binaries_util.BinaryManager, reduction_name: str = "reduction1", ) -> Path: test = test_util.metadata_read(test_dir_to_reduce) if not test.device or not test.device.name: raise AssertionError( f"Cannot reduce {str(test_dir_to_reduce)}; " f"device must be specified in {str(test_util.get_metadata_path(test_dir_to_reduce))}" ) if not test.crash_signature: raise AssertionError( f"Cannot reduce {str(test_dir_to_reduce)} because there is no crash string specified." ) # E.g. reports/crashes/no_signature/d50c96e8_opt_rand2_test_phone_ABC/results/phone_ABC/reductions/1 # Will contain work/ and source/ reduced_test_dir = test_util.get_reduced_test_dir( test_dir_reduction_output, test.device.name, reduction_name) source_dir = test_util.get_source_dir(test_dir_to_reduce) shader_jobs = tool.get_shader_jobs(source_dir) # TODO: if needed, this could become a parameter to this function. name_of_shader_to_reduce = shader_jobs[0].name if len(shader_jobs) > 1: check( len(shader_jobs) == 2 and shader_jobs[1].name == test_util.VARIANT_DIR, AssertionError( "Can only reduce tests with shader jobs reference and variant, or just variant." ), ) name_of_shader_to_reduce = shader_jobs[1].name reduction_work_variant_dir = run_glsl_reduce( source_dir=source_dir, name_of_shader_to_reduce=name_of_shader_to_reduce, output_dir=test_util.get_reduction_work_directory( reduced_test_dir, name_of_shader_to_reduce), binary_manager=binary_manager, preserve_semantics=preserve_semantics, ) final_reduced_shader_job_path = get_final_reduced_shader_job_path( reduction_work_variant_dir) check( final_reduced_shader_job_path.exists(), ReductionFailedError("Reduction failed.", reduction_name, reduction_work_variant_dir), ) # Finally, create the source_dir so the returned directory can be used as a test_dir. util.copy_dir(source_dir, test_util.get_source_dir(reduced_test_dir)) shader_job_util.copy( final_reduced_shader_job_path, test_util.get_shader_job_path(reduced_test_dir, name_of_shader_to_reduce), ) return reduced_test_dir
def run_shader_job( # pylint: disable=too-many-return-statements,too-many-branches, too-many-locals, too-many-statements; source_dir: Path, output_dir: Path, binary_manager: binaries_util.BinaryManager, test: Optional[Test] = None, device: Optional[Device] = None, ignore_test_and_device_binaries: bool = False, shader_job_overrides: Iterable[tool.NameAndShaderJob] = (), shader_job_shader_overrides: Optional[ tool.ShaderJobNameToShaderOverridesMap] = None, ) -> Path: if not shader_job_shader_overrides: shader_job_shader_overrides = {} with util.file_open_text(output_dir / "log.txt", "w") as log_file: try: gflogging.push_stream_for_logging(log_file) # TODO: Find amber path. NDK or host. # TODO: If Amber is going to be used, check if Amber can use Vulkan debug layers now, and if not, pass that # info down via a bool. if not test: test = test_util.metadata_read_from_path( source_dir / test_util.TEST_METADATA) if not device: device = test.device log(f"Running test on device:\n{device.name}") # We will create a binary_manager child with a restricted set of binaries so that we only use the binaries # specified in the test and by the device; if some required binaries are not specified by the test nor the # device, there will be an error instead of falling back to our default binaries. But we keep a reference to # the parent so we can still access certain "test-independent" binaries like Amber. binary_manager_parent = binary_manager if not ignore_test_and_device_binaries: binary_manager = binary_manager.get_child_binary_manager( list(device.binaries) + list(test.binaries)) spirv_opt_hash: Optional[str] = None spirv_opt_args: Optional[List[str]] = None if test.glsl.spirv_opt_args or test.spirv_fuzz.spirv_opt_args: spirv_opt_hash = binary_manager.get_binary_by_name( binaries_util.SPIRV_OPT_NAME).version spirv_opt_args = (list(test.glsl.spirv_opt_args) if test.glsl.spirv_opt_args else list( test.spirv_fuzz.spirv_opt_args)) shader_jobs = tool.get_shader_jobs(source_dir, overrides=shader_job_overrides) combined_spirv_shader_jobs: List[tool.SpirvCombinedShaderJob] = [] for shader_job in shader_jobs: try: shader_overrides = shader_job_shader_overrides.get( shader_job.name, None) combined_spirv_shader_jobs.append( tool.compile_shader_job( name=shader_job.name, input_json=shader_job.shader_job, work_dir=output_dir / shader_job.name, binary_paths=binary_manager, spirv_opt_args=spirv_opt_args, shader_overrides=shader_overrides, )) except subprocess.CalledProcessError: result_util.write_status(output_dir, fuzz.STATUS_TOOL_CRASH, shader_job.name) return output_dir except subprocess.TimeoutExpired: result_util.write_status(output_dir, fuzz.STATUS_TOOL_TIMEOUT, shader_job.name) return output_dir # Device types: |preprocess| and |shader_compiler| don't need an AmberScript file. # noinspection PyTypeChecker if device.HasField("preprocess"): # The "preprocess" device type just needs to get this far, so this is a success. result_util.write_status(output_dir, fuzz.STATUS_SUCCESS) return output_dir # noinspection PyTypeChecker if device.HasField("shader_compiler"): for combined_spirv_shader_job in combined_spirv_shader_jobs: try: shader_compiler_util.run_shader_job( device.shader_compiler, combined_spirv_shader_job.spirv_shader_job, output_dir, binary_manager=binary_manager, ) except subprocess.CalledProcessError: result_util.write_status( output_dir, fuzz.STATUS_CRASH, combined_spirv_shader_job.name, ) return output_dir except subprocess.TimeoutExpired: result_util.write_status( output_dir, fuzz.STATUS_TIMEOUT, combined_spirv_shader_job.name, ) return output_dir # The shader compiler succeeded on all files; this is a success. result_util.write_status(output_dir, fuzz.STATUS_SUCCESS) return output_dir # Other device types need an AmberScript file. amber_converter_shader_job_files = [ amber_converter.ShaderJobFile( name_prefix=combined_spirv_shader_job.name, asm_spirv_shader_job_json=combined_spirv_shader_job. spirv_asm_shader_job, glsl_source_json=combined_spirv_shader_job. glsl_source_shader_job, processing_info="", ) for combined_spirv_shader_job in combined_spirv_shader_jobs ] # Check if the first is the reference shader; if so, pull it out into its own variable. reference: Optional[amber_converter.ShaderJobFile] = None variants = amber_converter_shader_job_files if (amber_converter_shader_job_files[0].name_prefix == test_util.REFERENCE_DIR): reference = amber_converter_shader_job_files[0] variants = variants[1:] elif len(variants) > 1: raise AssertionError( "More than one variant, but no reference. This is unexpected." ) amber_script_file = amber_converter.spirv_asm_shader_job_to_amber_script( shader_job_file_amber_test=amber_converter. ShaderJobFileBasedAmberTest(reference_asm_spirv_job=reference, variants_asm_spirv_job=variants), output_amber_script_file_path=output_dir / "test.amber", amberfy_settings=amber_converter.AmberfySettings( spirv_opt_args=spirv_opt_args, spirv_opt_hash=spirv_opt_hash), ) is_compute = bool( shader_job_util.get_related_files( combined_spirv_shader_jobs[0].spirv_shader_job, [shader_job_util.EXT_COMP], )) # noinspection PyTypeChecker if device.HasField("host") or device.HasField("swift_shader"): icd: Optional[Path] = None # noinspection PyTypeChecker if device.HasField("swift_shader"): icd = binary_manager.get_binary_path_by_name( binaries_util.SWIFT_SHADER_NAME).path # Run the test on the host using Amber. host_device_util.run_amber( amber_script_file, output_dir, amber_path=binary_manager_parent.get_binary_path_by_name( binaries_util.AMBER_NAME).path, dump_image=(not is_compute), dump_buffer=is_compute, icd=icd, ) return output_dir # noinspection PyTypeChecker if device.HasField("android"): android_device.run_amber_on_device( amber_script_file, output_dir, dump_image=(not is_compute), dump_buffer=is_compute, serial=device.android.serial, ) return output_dir # TODO: For a remote device (which we will probably need to support), use log_a_file to output the # "amber_log.txt" file. raise AssertionError(f"Unhandled device type:\n{str(device)}") finally: gflogging.pop_stream_for_logging()
def maybe_add_report( # pylint: disable=too-many-locals; test_dir: Path, reports_dir: Path, device: Device, settings: Settings) -> Optional[Path]: result_output_dir = test_util.get_results_directory(test_dir, device.name) status = result_util.get_status(result_output_dir) report_subdirectory_name = "" if status == fuzz.STATUS_CRASH: report_subdirectory_name = "crashes" elif status == fuzz.STATUS_TOOL_CRASH: report_subdirectory_name = "tool_crashes" elif status == fuzz.STATUS_UNRESPONSIVE: report_subdirectory_name = "unresponsive" if not report_subdirectory_name: return None log_path = result_util.get_log_path(result_output_dir) log_contents = util.file_read_text(log_path) signature = signature_util.get_signature_from_log_contents(log_contents) signature_dir = reports_dir / report_subdirectory_name / signature util.mkdirs_p(signature_dir) # If the signature_dir contains a NOT_INTERESTING file, then don't bother creating a report. if (signature_dir / "NOT_INTERESTING").exists(): return None if signature != signature_util.BAD_IMAGE_SIGNATURE: # If we have reached the maximum number of crashes per signature for this device, don't create a report. num_duplicates = [ report_dir for report_dir in signature_dir.iterdir() if report_dir.is_dir() and report_dir.name.endswith(f"_{device.name}") ] if len(num_duplicates) >= settings.maximum_duplicate_crashes: return None # We include the device name in the directory name because it is possible that this test crashes on two # different devices but gives the same crash signature in both cases (e.g. for generic signatures # like "compile_error"). This would lead to two test copies having the same path. # It also means we can limit duplicates per device using the directory name. test_dir_in_reports = signature_dir / f"{test_dir.name}_{device.name}" util.copy_dir(test_dir, test_dir_in_reports) if signature != signature_util.BAD_IMAGE_SIGNATURE: # If we found a crash, rename the directories for all shaders other than the variant. Thus, only the variant # shader will run. bad_shader_name = result_util.get_status_bad_shader_name( test_util.get_results_directory(test_dir_in_reports, device.name)) # TODO: Could possibly improve this. Could try scanning the Amber log to figure out which shader failed? if not bad_shader_name: log("WARNING: assuming that the bad shader is the variant") bad_shader_name = test_util.VARIANT_DIR shader_jobs = tool.get_shader_jobs( test_util.get_source_dir(test_dir_in_reports)) found_bad_shader = False for shader_job in shader_jobs: if shader_job.name == bad_shader_name: found_bad_shader = True else: shader_job.shader_job.parent.rename( shader_job.shader_job.parent.parent / f"_{shader_job.name}") check( found_bad_shader, AssertionError( f"Could not find bad shader at: {test_util.get_source_dir(test_dir_in_reports) / bad_shader_name}" ), ) test_metadata = test_util.metadata_read(test_dir_in_reports) test_metadata.crash_signature = signature test_metadata.device.CopyFrom(device) test_metadata.expected_status = status test_util.metadata_write(test_metadata, test_dir_in_reports) return test_dir_in_reports
def run_reduction( source_dir_to_reduce: Path, reduction_output_dir: Path, binary_manager: binaries_util.BinaryManager, settings: Settings, ) -> Path: test = test_util.metadata_read_from_source_dir(source_dir_to_reduce) shader_jobs = tool.get_shader_jobs(source_dir_to_reduce) # TODO: if needed, this could become a parameter to this function. shader_job_to_reduce = shader_jobs[0] if len(shader_jobs) > 1: check( len(shader_jobs) == 2 and shader_jobs[1].name == test_util.VARIANT_DIR, AssertionError( "Can only reduce tests with shader jobs reference and variant, or just variant." ), ) shader_job_to_reduce = shader_jobs[1] shader_transformation_suffixes = shader_job_util.get_related_suffixes_that_exist( shader_job_to_reduce.shader_job, language_suffix=(shader_job_util.SUFFIX_TRANSFORMATIONS, ), ) shader_spv_suffixes = shader_job_util.get_related_suffixes_that_exist( shader_job_to_reduce.shader_job, language_suffix=(shader_job_util.SUFFIX_SPIRV, )) reduced_source_dir = source_dir_to_reduce for index, suffix in enumerate(shader_transformation_suffixes): # E.g. .frag.transformations -> .frag extension_to_reduce = str(Path(suffix).with_suffix("")) reduced_source_dir = run_reduction_part( reduction_part_output_dir=reduction_output_dir / f"0_{index}_{suffix.split('.')[1]}", source_dir_to_reduce=reduced_source_dir, shader_job_name_to_reduce=shader_job_to_reduce.name, extension_to_reduce=extension_to_reduce, preserve_semantics=True, binary_manager=binary_manager, settings=settings, ) if (test.crash_signature != signature_util.BAD_IMAGE_SIGNATURE and not settings.skip_spirv_reduce): for index, suffix in enumerate(shader_spv_suffixes): # E.g. .frag.spv -> .frag extension_to_reduce = str(Path(suffix).with_suffix("")) reduced_source_dir = run_reduction_part( reduction_part_output_dir=reduction_output_dir / f"1_{index}_{suffix.split('.')[1]}", source_dir_to_reduce=reduced_source_dir, shader_job_name_to_reduce=shader_job_to_reduce.name, extension_to_reduce=extension_to_reduce, preserve_semantics=False, binary_manager=binary_manager, settings=settings, ) # Create and return a symlink to the "best" reduction. return util.make_directory_symlink( new_symlink_file_path=reduction_output_dir / fuzz.BEST_REDUCTION_NAME, existing_dir=reduced_source_dir.parent, )
def run_reduction_on_report( # pylint: disable=too-many-locals; test_dir: Path, reports_dir: Path, binary_manager: binaries_util.BinaryManager) -> None: test = test_util.metadata_read(test_dir) check( bool(test.device and test.device.name), AssertionError( f"Cannot reduce {str(test_dir)}; " f"device must be specified in {str(test_util.get_metadata_path(test_dir))}" ), ) check( bool(test.crash_signature), AssertionError( f"Cannot reduce {str(test_dir)} because there is no crash string specified." ), ) source_dir = test_util.get_source_dir(test_dir) shader_jobs = tool.get_shader_jobs(source_dir) # TODO: if needed, this could become a parameter to this function. shader_job_to_reduce = shader_jobs[0] if len(shader_jobs) > 1: check( len(shader_jobs) == 2 and shader_jobs[1].name == test_util.VARIANT_DIR, AssertionError( "Can only reduce tests with shader jobs reference and variant, or just variant." ), ) shader_job_to_reduce = shader_jobs[1] shader_transformation_suffixes = shader_job_util.get_related_suffixes_that_exist( shader_job_to_reduce.shader_job, language_suffix=(shader_job_util.SUFFIX_TRANSFORMATIONS, ), ) shader_spv_suffixes = shader_job_util.get_related_suffixes_that_exist( shader_job_to_reduce.shader_job, language_suffix=(shader_job_util.SUFFIX_SPIRV, )) try: reduced_test = test_dir for index, suffix in enumerate(shader_transformation_suffixes): # E.g. .frag.transformations -> .frag extension_to_reduce = str(Path(suffix).with_suffix("")) reduced_test = run_reduction( test_dir_reduction_output=test_dir, test_dir_to_reduce=reduced_test, shader_job_name_to_reduce=shader_job_to_reduce.name, extension_to_reduce=extension_to_reduce, preserve_semantics=True, binary_manager=binary_manager, reduction_name=f"0_{index}_{suffix.split('.')[1]}", ) if test.crash_signature != signature_util.BAD_IMAGE_SIGNATURE: for index, suffix in enumerate(shader_spv_suffixes): # E.g. .frag.spv -> .frag extension_to_reduce = str(Path(suffix).with_suffix("")) reduced_test = run_reduction( test_dir_reduction_output=test_dir, test_dir_to_reduce=reduced_test, shader_job_name_to_reduce=shader_job_to_reduce.name, extension_to_reduce=extension_to_reduce, preserve_semantics=False, binary_manager=binary_manager, reduction_name=f"1_{index}_{suffix.split('.')[1]}", ) device_name = test.device.name # Create a symlink to the "best" reduction. best_reduced_test_link = test_util.get_reduced_test_dir( test_dir, device_name, fuzz.BEST_REDUCTION_NAME) util.make_directory_symlink( new_symlink_file_path=best_reduced_test_link, existing_dir=reduced_test) except ReductionFailedError as ex: # Create a symlink to the failed reduction so it is easy to investigate failed reductions. link_to_failed_reduction_path = ( reports_dir / "failed_reductions" / f"{test_dir.name}_{ex.reduction_name}") util.make_directory_symlink( new_symlink_file_path=link_to_failed_reduction_path, existing_dir=ex.reduction_work_dir, )