1#!/usr/bin/env python3 2""" 3# Created: Tue Jun 21 09:25:37 2022 (-0400) 4# @author: Jacob Faibussowitsch 5""" 6from __future__ import annotations 7 8import shutil 9import difflib 10import tempfile 11import traceback 12import petsclinter as pl 13 14from .main import ReturnCode 15from .classes._path import Path 16from .util._utility import traceback_format_exception 17 18from ._typing import * 19 20class TemporaryCopy: 21 __slots__ = 'fname', 'tmp', 'temp_path' 22 23 def __init__(self, fname: Path) -> None: 24 self.fname = fname.resolve(strict=True) 25 return 26 27 def __enter__(self) -> TemporaryCopy: 28 self.tmp = tempfile.NamedTemporaryFile(suffix=self.fname.suffix) 29 self.temp_path = Path(self.tmp.name).resolve(strict=True) 30 shutil.copy2(str(self.fname), str(self.temp_path)) 31 return self 32 33 def __exit__(self, *args, **kwargs) -> None: 34 self.orig_file().unlink(missing_ok=True) 35 self.rej_file().unlink(missing_ok=True) 36 del self.tmp 37 del self.temp_path 38 return 39 40 def orig_file(self) -> Path: 41 return self.temp_path.append_suffix('.orig') 42 43 def rej_file(self) -> Path: 44 return self.temp_path.append_suffix('.rej') 45 46def test_main( 47 petsc_dir: Path, 48 test_path: Path, 49 output_dir: Path, 50 patch_list: list[PathDiffPair], 51 errors_fixed: list[CondensedDiags], 52 errors_left: list[CondensedDiags], 53 replace: bool = False, 54) -> ReturnCode: 55 r"""The "main" function for testing 56 57 Parameters 58 ---------- 59 petsc_dir : 60 the path to $PETSC_DIR 61 test_path : 62 the path to test files 63 output_dir : 64 the path containing all of the output against which the generated output is compared to 65 patch_list : 66 the list of generated patches 67 errors_fixed : 68 the set of generated (but fixed) errors 69 errors_left : 70 the set of generated (and not fixed) errors 71 replace : 72 should the output be replaced? 73 74 Returns 75 ------- 76 ret : 77 `ReturnCode.ERROR_TEST_FAILED` if generated output does not match expected, and `ReturnCode.SUCCESS` 78 otherwise 79 """ 80 def test(generated_output: list[str], reference_file: Path) -> str: 81 short_ref_name = reference_file.relative_to(petsc_dir) 82 if replace: 83 pl.sync_print('\tREPLACE', short_ref_name) 84 reference_file.write_text(''.join(generated_output)) 85 return '' 86 if not reference_file.exists(): 87 return f'Missing reference file \'{reference_file}\'\n' 88 return ''.join( 89 difflib.unified_diff( 90 reference_file.read_text().splitlines(True), generated_output, 91 fromfile=str(short_ref_name), tofile='Generated Output', n=0 92 ) 93 ) 94 95 # sanitize the output so that it will be equal across systems 96 def sanitize_output_file(text: Optional[str]) -> list[str]: 97 return [] if text is None else [l.replace(str(petsc_dir), '.') for l in text.splitlines(True)] 98 99 def sanitize_patch_file(text: Optional[str]) -> list[str]: 100 # skip the diff header with file names 101 return [] if text is None else text.splitlines(True)[2:] 102 103 def rename_patch_file_target(text: str, new_path: Path) -> str: 104 lines = text.splitlines(True) 105 out_file = lines[0].split()[1] 106 lines[0] = lines[0].replace(out_file, str(new_path)) 107 lines[1] = lines[1].replace(out_file, str(new_path)) 108 return ''.join(lines) 109 110 FIXED_MARKER = '<--- FIXED --->' 111 LEFT_MARKER = '<--- LEFT --->' 112 patch_error = {} 113 root_dir = f'--directory={petsc_dir.anchor}' 114 patches = dict(patch_list) 115 116 tmp_output = { 117 p : [FIXED_MARKER, '\n'.join(s), LEFT_MARKER] 118 for diags in errors_fixed 119 for p, s in diags.items() 120 } 121 for diags in errors_left: 122 for path, strlist in diags.items(): 123 if path not in tmp_output: 124 tmp_output[path] = [f'{FIXED_MARKER}\n{LEFT_MARKER}'] 125 tmp_output[path].extend(strlist) 126 127 # ensure that each output ends with a newline 128 output = { 129 path : '\n'.join(strlist if strlist[-1].endswith('\n') else strlist + ['']) 130 for path, strlist in tmp_output.items() 131 } 132 # output = { 133 # path : '\n'.join(strlist if len(strlist) == 4 else strlist + ['']) for path, strlist in tmp_output.items() 134 # } 135 #output = {key : '\n'.join(val if len(val) == 4 else val + ['']) for key, val in output.items()} 136 if test_path.is_dir(): 137 c_suffixes = (r'*.c', r'*.cxx', r'*.cc', r'*.CC') 138 file_list = [item for sublist in map(test_path.glob, c_suffixes) for item in sublist] 139 else: 140 file_list = [test_path] 141 for test_file in file_list: 142 output_base = output_dir / test_file.stem 143 output_file = output_base.with_suffix('.out') 144 patch_file = output_base.with_suffix('.patch') 145 short_name = test_file.relative_to(petsc_dir) 146 147 pl.sync_print('\tTEST ', short_name) 148 149 output_errors = [ 150 test(sanitize_output_file(output.get(test_file)), output_file), 151 test(sanitize_patch_file(patches.get(test_file)), patch_file) 152 ] 153 154 # no point in checking the patch, we have already replaced 155 if not replace: 156 # make sure the patch can be applied 157 with TemporaryCopy(test_file) as tmp_src, \ 158 tempfile.NamedTemporaryFile(delete=True, suffix='.patch') as temp_patch: 159 tmp_patch_path = Path(temp_patch.name).resolve(strict=True) 160 try: 161 tmp_patch_path.write_text(rename_patch_file_target(patches[test_file], tmp_src.temp_path)) 162 pl.util.subprocess_capture_output( 163 ['patch', root_dir, '--strip=0', '--unified', f'--input={tmp_patch_path}'] 164 ) 165 except Exception as exc: 166 exception = ''.join(traceback_format_exception(exc)) 167 emess = f'Application of patch based on {test_file} failed:\n{exception}\n' 168 rej = tmp_src.rej_file() 169 if rej.exists(): 170 emess += f'\n{rej}:\n{rej.read_text()}' 171 output_errors.append(emess) 172 173 output_errors = [e for e in output_errors if e] 174 if output_errors: 175 pl.sync_print('\tNOT OK ', short_name) 176 patch_error[test_file] = '\n'.join(output_errors) 177 else: 178 pl.sync_print('\tOK ', short_name) 179 if patch_error: 180 err_str = f"[ERROR] {85 * '-'} [ERROR]" 181 err_bars = (err_str + '\n', err_str) 182 for err_file in patch_error: 183 pl.sync_print(patch_error[err_file].join(err_bars)) 184 return ReturnCode.ERROR_TEST_FAILED 185 return ReturnCode.SUCCESS 186