* 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:
Scott Beddall 2023-10-25 11:32:29 -07:00 коммит произвёл GitHub
Родитель a0291e3249
Коммит 760a3352a0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 369 добавлений и 0 удалений

52
.ci/regression-steps.yml Normal file
Просмотреть файл

@ -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 }}"

106
.ci/run-full-regression.yml Normal file
Просмотреть файл

@ -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)