Adding a Regression Job (#1012)
* Add new regression pipeline. This workload runs on four pipelines, Two target versions of oav, both validate-spec and validate-example on each. Within each pipeline, we clone the complete azure-rest-api-specs repository and invoke the chosen oav version + check against each spec present within the repo * Finally, we diff the outputs, to catch any OAV behavior regressions Co-authored-by: semick-dev <sbeddall@gmail.com>
This commit is contained in:
Родитель
a0291e3249
Коммит
760a3352a0
|
@ -0,0 +1,52 @@
|
|||
parameters:
|
||||
- name: TargetVersion
|
||||
type: string
|
||||
default: 'LOCAL' #specialcase value that will install the local version of oav
|
||||
- name: Type
|
||||
type: string
|
||||
|
||||
|
||||
# assume presence of variable $(OutputFolder)
|
||||
# $(RestSpecsRepo)
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
mkdir -p $(OutputFolder)
|
||||
mkdir -p $(RestSpecsRepo)
|
||||
displayName: Create Folders
|
||||
|
||||
- task: UseNode@1
|
||||
inputs:
|
||||
versionSpec: '16.x'
|
||||
checkLatest: true
|
||||
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.11'
|
||||
inputs:
|
||||
versionSpec: "3.11"
|
||||
|
||||
- bash: |
|
||||
if [[ "${{ parameters.TargetVersion }}" == "LOCAL" ]]; then
|
||||
npm ci
|
||||
npm run build
|
||||
npm link
|
||||
else
|
||||
npm install -g oav@${{ parameters.TargetVersion }}
|
||||
fi
|
||||
displayName: Install OAV
|
||||
workingDirectory: $(Build.SourcesDirectory)
|
||||
|
||||
- bash: |
|
||||
git clone https://github.com/azure/azure-rest-api-specs.git --depth=1 $(RestSpecsRepo)
|
||||
displayName: Clone the Git Repo
|
||||
|
||||
- bash: |
|
||||
python $(Build.SourcesDirectory)/.ci/scripts/run_oav_regression.py --oav oav --target $(RestSpecsRepo) --output $(OutputFolder) --type "${{ parameters.Type }}"
|
||||
displayName: Run the Regression Script
|
||||
timeoutInMinutes: 350
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
condition: always()
|
||||
inputs:
|
||||
pathtoPublish: $(OutputFolder)
|
||||
artifactName: "${{ parameters.TargetVersion }}-${{ parameters.Type }}"
|
|
@ -0,0 +1,106 @@
|
|||
# TODO: refactor the hilariously common jobs
|
||||
|
||||
parameters:
|
||||
- name: BeforeVersion
|
||||
type: string
|
||||
default: ''
|
||||
- name: AfterVersion
|
||||
type: string
|
||||
default: 'LOCAL' #specialcase value that will install the local version of oav
|
||||
|
||||
pool:
|
||||
name: azsdk-pool-mms-ubuntu-2004-general
|
||||
vmImage: MMSUbuntu20.04
|
||||
|
||||
jobs:
|
||||
- job: RegressionASpecs
|
||||
variables:
|
||||
- name: OutputFolder
|
||||
value: $(Build.ArtifactStagingDirectory)/${{ parameters.BeforeVersion }}
|
||||
- name: RestSpecsRepo
|
||||
value: $(Build.ArtifactStagingDirectory)/azure-rest-api-specs
|
||||
timeoutInMinutes: 360
|
||||
displayName: Run ${{ parameters.BeforeVersion }} Specs
|
||||
steps:
|
||||
- template: ./regression-steps.yml
|
||||
parameters:
|
||||
TargetVersion: ${{ parameters.BeforeVersion }}
|
||||
Type: "validate-spec"
|
||||
|
||||
- job: RegressionAExamples
|
||||
variables:
|
||||
- name: OutputFolder
|
||||
value: $(Build.ArtifactStagingDirectory)/${{ parameters.BeforeVersion }}
|
||||
- name: RestSpecsRepo
|
||||
value: $(Build.ArtifactStagingDirectory)/azure-rest-api-specs
|
||||
timeoutInMinutes: 360
|
||||
displayName: Run ${{ parameters.BeforeVersion }} Examples
|
||||
steps:
|
||||
- template: ./regression-steps.yml
|
||||
parameters:
|
||||
TargetVersion: ${{ parameters.BeforeVersion }}
|
||||
Type: "validate-example"
|
||||
|
||||
- job: RegressionBSpecs
|
||||
variables:
|
||||
- name: OutputFolder
|
||||
value: $(Build.ArtifactStagingDirectory)/${{ parameters.AfterVersion }}
|
||||
- name: RestSpecsRepo
|
||||
value: $(Build.ArtifactStagingDirectory)/azure-rest-api-specs
|
||||
timeoutInMinutes: 360
|
||||
displayName: Run ${{ parameters.AfterVersion }} Specs
|
||||
steps:
|
||||
- template: ./regression-steps.yml
|
||||
parameters:
|
||||
TargetVersion: ${{ parameters.AfterVersion }}
|
||||
Type: "validate-spec"
|
||||
|
||||
- job: RegressionBExamples
|
||||
variables:
|
||||
- name: OutputFolder
|
||||
value: $(Build.ArtifactStagingDirectory)/${{ parameters.AfterVersion }}
|
||||
- name: RestSpecsRepo
|
||||
value: $(Build.ArtifactStagingDirectory)/azure-rest-api-specs
|
||||
timeoutInMinutes: 360
|
||||
displayName: Run ${{ parameters.AfterVersion }} Examples
|
||||
steps:
|
||||
- template: ./regression-steps.yml
|
||||
parameters:
|
||||
TargetVersion: ${{ parameters.AfterVersion }}
|
||||
Type: "validate-example"
|
||||
|
||||
- job: Summarize
|
||||
timeoutInMinutes: 180
|
||||
dependsOn:
|
||||
- RegressionASpecs
|
||||
- RegressionBSpecs
|
||||
- RegressionAExamples
|
||||
- RegressionBExamples
|
||||
displayName: Run Diff
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
artifactName: "${{ parameters.BeforeVersion }}-validate-spec"
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/before
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
artifactName: "${{ parameters.BeforeVersion }}-validate-example"
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/before
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
artifactName: "${{ parameters.AfterVersion }}-validate-spec"
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/after
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
artifactName: "${{ parameters.AfterVersion }}-validate-example"
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/after
|
||||
- bash: |
|
||||
echo "CACHE BEFORE"
|
||||
cat $(Build.ArtifactStagingDirectory)/before/.spec_cache && rm $(Build.ArtifactStagingDirectory)/before/.spec_cache
|
||||
|
||||
echo "CACHE AFTER"
|
||||
cat $(Build.ArtifactStagingDirectory)/after/.spec_cache && rm $(Build.ArtifactStagingDirectory)/after/.spec_cache
|
||||
displayName: Dump Spec Caches
|
||||
- bash: |
|
||||
diff -qr $(Build.ArtifactStagingDirectory)/before $(Build.ArtifactStagingDirectory)/after
|
||||
displayName: Diff outputs
|
|
@ -0,0 +1,211 @@
|
|||
# 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)
|
Загрузка…
Ссылка в новой задаче