xref: /petsc/lib/petsc/bin/maint/petsclinter/petsclinter/test_main.py (revision daba9d70159ea2f6905738fcbec7404635487b2b)
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