eng/scripts/run_oav_regression.py (146 lines of code) (raw):

# This script is used to invoke oav (examples AND specification) against every specification # discovered within a target repo. # # It is intended to be used twice, with different invoking versions of oav. # # FOR COMPATIBILITY The script expects that: # - node 16+ is on the PATH import glob import argparse import os import shutil import sys import tempfile import difflib from dataclasses import dataclass import subprocess from typing import List, Dict, Tuple CACHE_FILE_NAME: str = ".spec_cache" @dataclass class OAVScanResult: """Used to track the results of an oav invocation""" target_folder: str stdout: str stderr: str success: int oav_version: str @property def stdout_length_in_bytes(self) -> int: return os.path.getsize(self.stdout) @property def stderr_length_in_bytes(self) -> int: return os.path.getsize(self.stderr) def get_oav_output( oav_exe: str, target_folder: str, collection_std_out: str, collection_std_err: str, oav_command: str, oav_version: str, ) -> OAVScanResult: try: with open(collection_std_out, "w", encoding="utf-8") as out, open( collection_std_err, "w", encoding="utf-8" ) as err: print([oav_exe, oav_command, target_folder]) result = subprocess.run( [oav_exe, oav_command, target_folder], capture_output=True, check=True, text=True ) out.write(result.stdout) err.write(result.stderr) return OAVScanResult(target_folder, collection_std_out, collection_std_err, result.returncode, oav_version) except subprocess.CalledProcessError as e: with open(collection_std_err, "a", encoding="utf-8") as err: err.write(str(e)) return OAVScanResult(target_folder, collection_std_out, collection_std_err, -1, oav_version) def is_word_present_in_file(file_path, word): try: with open(file_path, "rb") as file: first_bytes = file.read(20) return word in first_bytes except Exception as e: return False def get_specification_files(target_folder: str, output_folder: str) -> List[str]: target = os.path.join(target_folder, "specification", "**", "*.json") jsons = glob.glob(target, recursive=True) search_word = b"swagger" specs = [] num = len(jsons) output_cache = os.path.join(output_folder, CACHE_FILE_NAME) if os.path.exists(output_cache): with open(output_cache, "r", encoding="utf-8") as c: specs = c.readlines() return [spec.strip() for spec in specs] print(f"Scanned directory, found {len(jsons)} json files.") for index, json_file in enumerate(jsons): if is_word_present_in_file(json_file, search_word): specs.append(json_file) print(f"Filtered to {len(specs)} swagger files.") with open(output_cache, "w", encoding="utf-8") as c: c.write("\n".join(specs)) return specs def verify_oav_version(oav: str) -> str: try: result = subprocess.run([oav, "--version"], capture_output=True, shell=True) return result.stdout.decode("utf-8").strip() except Exception as f: return "-1" def get_output_files(root_target_folder: str, choice: str, target_folder: str) -> Tuple[str, str]: """Given the root of the azure-rest-api-specs repo AND a folder that is some deeper child of that, come up with the output file names""" relpath = os.path.relpath(target_folder, root_target_folder) flattened_path = relpath.replace("\\", "_").replace("/", "_").replace(".json", "") return (f"{flattened_path}_{choice}_out.log", f"{flattened_path}_{choice}_err.log") def prepare_output_folder(target_folder: str) -> str: must_repopulate_cache = False if os.path.exists(target_folder): cache_file = os.path.join(target_folder, CACHE_FILE_NAME) if os.path.exists(cache_file): tmp_dir = tempfile.gettempdir() cache_location = os.path.join(tmp_dir, CACHE_FILE_NAME) shutil.move(cache_file, cache_location) must_repopulate_cache = True shutil.rmtree(target_folder) os.makedirs(target_folder) if must_repopulate_cache: shutil.move(cache_location, cache_file) return target_folder def dump_summary(summary: Dict[str, OAVScanResult]) -> None: print(f"Scanned {len(summary.keys())} files successfully.") def run(oav_exe: str, spec: str, output_folder: str, choice: str, oav_version: str): collection_stdout_file, collection_stderr_file = get_output_files(args.target, choice, spec) resolved_out = os.path.join(output_folder, collection_stdout_file) resolved_err = os.path.join(output_folder, collection_stderr_file) summary[f"{spec}-{choice}"] = get_oav_output(oav_exe, spec, resolved_out, resolved_err, choice, oav_version) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Scan azure-rest-api-specs repository, invoke oav") parser.add_argument( "--target", dest="target", help="The azure-rest-api-specs repo root.", required=True, ) parser.add_argument( "--output", dest="output", help="The folder which will contain the oav output.", required=True, ) parser.add_argument( "--type", dest="type", required=True, help="Are we running specs or examples?", choices=["validate-spec","validate-example"] ) parser.add_argument( "--oav", dest="oav", help="The oav exe this script will be using! If OAV is on the PATH just pass nothing!", required=False ) args = parser.parse_args() if args.oav: oav_exe = args.oav else: oav_exe = "oav" oav_version = verify_oav_version(oav_exe) if oav_version == "-1": print("OAV is not available on the PATH. Resolve this ane reinvoke.") sys.exit(1) output_folder: str = prepare_output_folder(args.output) specs: List[str] = get_specification_files(args.target, output_folder) summary: Dict[str, OAVScanResult] = {} for spec in specs: run(oav_exe, spec, output_folder, args.type, oav_version) dump_summary(summary)