def __init__(self, filedir: str) -> None: self.cwd = os.path.dirname( filedir) # base directory for indir, outdir and tempdir self.indir = os.path.join(self.cwd, 'input') # Input files self.outdir = os.path.join(self.cwd, 'output') # Expected/actual output files self.tempdir = os.path.join( self.cwd, 'temp') # Scratch directory for temporary work # Get the parent's directory name. If it is a test directory, borrow from its environment parent = Path(self.cwd).parts[-2] if parent.startswith('test'): parent_env = import_module('..environment', __package__) self.meta_yaml = parent_env.env.meta_yaml self.types_yaml = parent_env.env.types_yaml self.mapping_yaml = parent_env.env.mapping_yaml self.import_map = parent_env.env.import_map self.mismatch_action = parent_env.env.mismatch_action self.root_input_path = parent_env.env.root_input_path self.root_expected_path = parent_env.env.root_expected_path self.root_temp_file_path = parent_env.env.root_temp_file_path self._log = parent_env.env._log else: self.meta_yaml = self.input_path('meta.yaml') self._check_changed(self.meta_yaml, LOCAL_METAMODEL_YAML_FILE) self.types_yaml = self.input_path('includes', TYPES_FILE_NAME) self._check_changed(self.types_yaml, LOCAL_TYPES_YAML_FILE) self.mapping_yaml = self.input_path('includes', MAPPING_FILE_NAME) self._check_changed(self.mapping_yaml, LOCAL_MAPPING_YAML_FILE) from tests import USE_LOCAL_IMPORT_MAP self.import_map = self.input_path( 'local_import_map.json') if USE_LOCAL_IMPORT_MAP else None from tests import DEFAULT_MISMATCH_ACTION self.mismatch_action = DEFAULT_MISMATCH_ACTION self.root_input_path = self.input_path self.root_expected_path = self.expected_path self.root_temp_file_path = self.temp_file_path self._log = MismatchLog()
def clear_log(self) -> None: """ Clear the output log """ self._log = MismatchLog()
class TestEnvironment: import_map_warning_emitted: bool = False """ Testing environment """ def __init__(self, filedir: str) -> None: self.cwd = os.path.dirname( filedir) # base directory for indir, outdir and tempdir self.indir = os.path.join(self.cwd, 'input') # Input files self.outdir = os.path.join(self.cwd, 'output') # Expected/actual output files self.tempdir = os.path.join( self.cwd, 'temp') # Scratch directory for temporary work # Get the parent's directory name. If it is a test directory, borrow from its environment parent = Path(self.cwd).parts[-2] if parent.startswith('test'): parent_env = import_module('..environment', __package__) self.meta_yaml = parent_env.env.meta_yaml self.types_yaml = parent_env.env.types_yaml self.mapping_yaml = parent_env.env.mapping_yaml self.import_map = parent_env.env.import_map self.mismatch_action = parent_env.env.mismatch_action self.root_input_path = parent_env.env.root_input_path self.root_expected_path = parent_env.env.root_expected_path self.root_temp_file_path = parent_env.env.root_temp_file_path self._log = parent_env.env._log else: self.meta_yaml = self.input_path('meta.yaml') self._check_changed(self.meta_yaml, LOCAL_METAMODEL_YAML_FILE) self.types_yaml = self.input_path('includes', TYPES_FILE_NAME) self._check_changed(self.types_yaml, LOCAL_TYPES_YAML_FILE) self.mapping_yaml = self.input_path('includes', MAPPING_FILE_NAME) self._check_changed(self.mapping_yaml, LOCAL_MAPPING_YAML_FILE) from tests import USE_LOCAL_IMPORT_MAP self.import_map = self.input_path( 'local_import_map.json') if USE_LOCAL_IMPORT_MAP else None from tests import DEFAULT_MISMATCH_ACTION self.mismatch_action = DEFAULT_MISMATCH_ACTION self.root_input_path = self.input_path self.root_expected_path = self.expected_path self.root_temp_file_path = self.temp_file_path self._log = MismatchLog() @staticmethod def _check_changed(test_file: str, runtime_file: str) -> None: if not filecmp.cmp(test_file, runtime_file): print( f"WARNING: Test file {test_file} does not match {runtime_file}. " f"You may want to update the test version and rerun") from tests import USE_LOCAL_IMPORT_MAP if USE_LOCAL_IMPORT_MAP and not TestEnvironment.import_map_warning_emitted: print( f"WARNING: USE_LOCAL_IMPORT_MAP must be reset to False before completing submission." ) TestEnvironment.import_map_warning_emitted = True def clear_log(self) -> None: """ Clear the output log """ self._log = MismatchLog() def input_path(self, *path: str) -> str: """ Create a file path in the local input directory """ return os.path.join(self.indir, *[p for p in path if p]) def expected_path(self, *path: str) -> str: """ Create a file path in the local output directory """ return os.path.join(self.outdir, *[p for p in path if p]) def actual_path(self, *path: str, is_dir: bool = False) -> str: """ Return the full path to the path fragments in path """ dir_path = [p for p in (path if is_dir else path[:-1]) if p] self.make_temp_dir(*dir_path) return os.path.join(self.tempdir, *[p for p in path if p]) def temp_file_path(self, *path: str, is_dir: bool = False) -> str: """ Create the directories down to the path fragments in path. If is_dir is True, create and clear the innermost directory """ return self.actual_path(*path, is_dir=is_dir) def log(self, file_or_directory: str, message: Optional[str] = None) -> None: self._log.log(file_or_directory, message) @property def verb(self) -> str: return 'will be' if self.fail_on_error else 'was' @property def fail_on_error(self) -> bool: return self.mismatch_action == MismatchAction.Fail @property def report_errors(self) -> bool: return self.mismatch_action != MismatchAction.Ignore def __str__(self): """ Return the current state of the log file """ return '\n\n'.join([str(e) for e in self._log.entries]) def make_temp_dir(self, *paths: str) -> str: """ Create and initialize a list of paths """ full_path = self.tempdir TestEnvironment.make_testing_directory(full_path) if len(paths): for i in range(len(paths)): full_path = os.path.join(full_path, paths[i]) TestEnvironment.make_testing_directory(full_path, clear=i == len(paths) - 1) return full_path def string_comparator(self, expected: str, actual: str) -> Optional[str]: """ Compare two strings w/ embedded line feeds. Return a simple match/nomatch output message :param expected: expected string :param actual: actual string :return: Error message if mismatch else None """ if expected.replace('\r\n', '\n').strip() != actual.replace( '\r\n', '\n').strip(): return f"Output {self.verb} changed." @staticmethod def remove_testing_directory(directory: str) -> None: shutil.rmtree(directory, ignore_errors=True) @staticmethod def make_testing_directory(directory: str, clear: bool = False) -> None: """ Create directory if necessary and clear it if requested :param directory: Directory to create :param clear: True means remove everything there """ if clear or not os.path.exists(directory): safety_file = os.path.join(directory, "generated") if os.path.exists(directory): if not os.path.exists(safety_file): raise FileNotFoundError( f"'generated' guard file not found in {directory}") shutil.rmtree(directory) os.makedirs(directory, exist_ok=True) with open(safety_file, "w") as f: f.write( "Generated for safety. Directory will not be cleared if this file is not present" ) def generate_directory(self, dirname: Union[str, List[str]], generator: Callable[[str], None]) -> None: """ Invoke the generator and compare the output in a temp directory to the output directory. Report the results and then update the output directory :param dirname: relative directory name (e.g. gengolr/meta) :param generator: function to create the output. First argument is the target directory """ dirname = dirname if isinstance(dirname, List) else [dirname] temp_output_directory = self.make_temp_dir(*dirname) expected_output_directory = self.expected_path(*dirname) self.make_testing_directory(expected_output_directory) generator(temp_output_directory) diffs = are_dir_trees_equal(expected_output_directory, temp_output_directory) if diffs: self.log(expected_output_directory, diffs) if not self.fail_on_error: shutil.rmtree(expected_output_directory) os.rename(temp_output_directory, expected_output_directory) else: shutil.rmtree(temp_output_directory) def generate_single_file(self, filename: Union[str, List[str]], generator: Callable[[Optional[str]], Optional[str]], value_is_returned: bool = False, filtr: Callable[[str], str] = None, comparator: Callable[[str, str], str] = None, use_testing_root: bool = False) -> str: """ Invoke the generator and compare the actual results to the expected. :param filename: relative file name(s) (no path) :param generator: output generator. Either produces a string or creates a file :param value_is_returned: True means that generator returns output directly :param filtr: Optional filter to remove non-compare information (e.g. dates, specific paths, etc.) :param comparator: Optional output comparison function. :param use_testing_root: True means output directory is in test root instead of local directory :return: the generator output """ # If no filter, default to identity function if not filtr: filtr = lambda s: s filename = filename if isinstance(filename, List) else [filename] actual_file = self.root_temp_file_path( *filename) if use_testing_root else self.actual_path(*filename) expected_file = self.root_expected_path( *filename) if use_testing_root else self.expected_path(*filename) if value_is_returned: actual = filtr(generator()) else: outf = StringIO() from tests import CLIExitException with contextlib.redirect_stdout(outf): try: generator() except CLIExitException: pass actual = filtr(outf.getvalue()) if not self.eval_single_file( expected_file, actual, filtr, comparator if comparator else self.string_comparator): if self.fail_on_error: with open(actual_file, 'w') as actualf: actualf.write(actual) return actual def eval_single_file(self, expected_file_path: str, actual_text: str, filtr: Callable[[str], str], comparator: Callable[[str, str], str] = None) -> bool: """ Compare actual_text to the contents of the expected file. Log a message if there is a mismatch and overwrite the expected file if we're not in the fail on error mode """ if comparator is None: comparator = self.string_comparator if os.path.exists(expected_file_path): with open(expected_file_path) as expf: expected_text = filtr(expf.read()) msg = comparator(expected_text, filtr(actual_text)) else: msg = f"New file {self.verb} created" if msg: self.log(expected_file_path, msg) if msg and not self.fail_on_error: with open(expected_file_path, 'w') as outf: outf.write(actual_text) return not msg