Internal: New test setup using snapshots to validate produced models (#365)
This commit is contained in:
Родитель
8b75335858
Коммит
b95da6bb05
|
@ -41,6 +41,10 @@ steps:
|
|||
|
||||
displayName: "Rush install, build and test"
|
||||
|
||||
- script: npm run test:v2:ci
|
||||
workingDirectory: modelerfour
|
||||
displayName: "Test V2"
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: "$(Build.SourcesDirectory)/common/temp/artifacts/packages/autorest-modelerfour-$(artver).tgz"
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// @ts-check
|
||||
|
||||
/** @type {jest.InitialOptions} */
|
||||
const config = {
|
||||
transform: {
|
||||
"^.+\\.ts$": "ts-jest",
|
||||
},
|
||||
moduleFileExtensions: ["ts", "js", "json", "node"],
|
||||
moduleNameMapper: {},
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ["src/**/*.ts", "!**/node_modules/**"],
|
||||
coverageReporters: ["json", "lcov", "cobertura", "text", "html", "clover"],
|
||||
coveragePathIgnorePatterns: ["/node_modules/", ".*/test/.*"],
|
||||
modulePathIgnorePatterns: ["<rootDir>/sdk"],
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig: "tsconfig.json",
|
||||
},
|
||||
},
|
||||
setupFilesAfterEnv: ["<rootDir>/testv2/setupJest.ts"],
|
||||
testMatch: ["<rootDir>/testv2/**/*.test.ts"],
|
||||
verbose: true,
|
||||
testEnvironment: "node",
|
||||
};
|
||||
|
||||
module.exports = config;
|
|
@ -2156,19 +2156,9 @@ export class ModelerFour {
|
|||
const kmtJSON = groupedMediaTypes.get(KnownMediaType.Json);
|
||||
if (kmtJSON) {
|
||||
if ([...kmtJSON.values()].find((x) => x.schema.instance && this.isSchemaBinary(x.schema.instance))) {
|
||||
this.processBinary(
|
||||
KnownMediaType.Binary,
|
||||
kmtJSON,
|
||||
operation,
|
||||
requestBody
|
||||
);
|
||||
this.processBinary(KnownMediaType.Binary, kmtJSON, operation, requestBody);
|
||||
} else {
|
||||
this.processSerializedObject(
|
||||
KnownMediaType.Json,
|
||||
kmtJSON,
|
||||
operation,
|
||||
requestBody
|
||||
);
|
||||
this.processSerializedObject(KnownMediaType.Json, kmtJSON, operation, requestBody);
|
||||
}
|
||||
}
|
||||
const kmtXML = groupedMediaTypes.get(KnownMediaType.Xml);
|
||||
|
@ -2285,11 +2275,7 @@ export class ModelerFour {
|
|||
}
|
||||
|
||||
private isSchemaBinary(schema: OpenAPI.Schema) {
|
||||
return (
|
||||
<any>schema.type === "file" ||
|
||||
<any>schema.format === "file" ||
|
||||
<any>schema.format === "binary"
|
||||
);
|
||||
return <any>schema.type === "file" || <any>schema.format === "file" || <any>schema.format === "binary";
|
||||
}
|
||||
|
||||
private propagateSchemaUsage(schema: Schema): void {
|
||||
|
|
|
@ -20,7 +20,9 @@
|
|||
"build": "tsc -p .",
|
||||
"prepack": "npm run static-link && npm run build",
|
||||
"unit-test": "npm run build && mocha dist/test/unit",
|
||||
"test": "npm run build && mocha dist/test --timeout=200000 && mocha dist/test/unit"
|
||||
"test": "npm run build && mocha dist/test --timeout=200000 && mocha dist/test/unit",
|
||||
"test:v2": "jest",
|
||||
"test:v2:ci": "jest --ci"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -41,9 +43,10 @@
|
|||
"@types/js-yaml": "3.12.1",
|
||||
"@types/mocha": "5.2.5",
|
||||
"@types/node": "12.7.2",
|
||||
"@types/jest": "26.0.16",
|
||||
"mocha": "5.2.0",
|
||||
"mocha-typescript": "1.1.17",
|
||||
"typescript": "~3.7.2",
|
||||
"typescript": "~3.9.7",
|
||||
"@typescript-eslint/eslint-plugin": "~2.6.0",
|
||||
"@typescript-eslint/parser": "~2.6.0",
|
||||
"eslint": "~6.6.0",
|
||||
|
@ -52,6 +55,10 @@
|
|||
"@microsoft.azure/autorest.testserver": "~2.10.46",
|
||||
"@azure-tools/uri": "~3.0.0",
|
||||
"js-yaml": "3.13.1",
|
||||
"jest": "~26.6.3",
|
||||
"jest-snapshot": "~26.6.2",
|
||||
"expect": "~26.6.2",
|
||||
"ts-jest": "~26.4.4",
|
||||
"@azure-tools/codegen": "~2.5.0",
|
||||
"@azure-tools/codegen-csharp": "~3.0.0",
|
||||
"@azure-tools/autorest-extension-base": "~3.1.0",
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import { MatcherState } from "expect";
|
||||
import * as fs from "fs";
|
||||
import SnapshotState from "jest-snapshot/build/State";
|
||||
import * as path from "path";
|
||||
|
||||
declare global {
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
toMatchRawFileSnapshot(snapshotFile: string): CustomMatcherResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAbsolutePathToSnapshot(testPath: string, snapshotFile: string) {
|
||||
return path.isAbsolute(snapshotFile) ? snapshotFile : path.resolve(path.dirname(testPath), snapshotFile);
|
||||
}
|
||||
|
||||
type Context = jest.MatcherUtils &
|
||||
MatcherState & {
|
||||
snapshotState: SnapshotState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper
|
||||
*/
|
||||
function toMatchRawFileSnapshot(
|
||||
this: Context,
|
||||
received: object | Array<object>,
|
||||
filename: string,
|
||||
): jest.CustomMatcherResult {
|
||||
if (typeof received !== "string") {
|
||||
throw new Error("toMatchRawFileSnapshot is only supported with raw text");
|
||||
}
|
||||
|
||||
if (this.isNot) {
|
||||
return {
|
||||
pass: true, // Will get inverted because of the .not
|
||||
message: () => `.${this.utils.BOLD_WEIGHT("not")} cannot be used with snapshot matchers`,
|
||||
};
|
||||
}
|
||||
|
||||
const filepath = getAbsolutePathToSnapshot(this.testPath!, filename);
|
||||
const content: string = received;
|
||||
const updateSnapshot: "none" | "all" | "new" = (this.snapshotState as any)._updateSnapshot;
|
||||
|
||||
const coloredFilename = this.utils.DIM_COLOR(filename);
|
||||
const errorColor = this.utils.RECEIVED_COLOR;
|
||||
|
||||
if (updateSnapshot === "none" && !fs.existsSync(filepath)) {
|
||||
// We're probably running in CI environment
|
||||
|
||||
this.snapshotState.unmatched++;
|
||||
|
||||
return {
|
||||
pass: false,
|
||||
message: () =>
|
||||
`New output file ${coloredFilename} was ${errorColor("not written")}.\n\n` +
|
||||
"The update flag must be explicitly passed to write a new snapshot.\n\n",
|
||||
};
|
||||
}
|
||||
|
||||
if (fs.existsSync(filepath)) {
|
||||
const output = fs.readFileSync(filepath, "utf8").replace(/\r\n/g, "\n");
|
||||
// The matcher is being used with `.not`
|
||||
if (output === content) {
|
||||
this.snapshotState.matched++;
|
||||
return { pass: true, message: () => "" };
|
||||
} else {
|
||||
if (updateSnapshot === "all") {
|
||||
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
||||
fs.writeFileSync(filepath, content);
|
||||
|
||||
this.snapshotState.updated++;
|
||||
|
||||
return { pass: true, message: () => "" };
|
||||
} else {
|
||||
this.snapshotState.unmatched++;
|
||||
|
||||
return {
|
||||
pass: false,
|
||||
message: () =>
|
||||
`Received content ${errorColor("doesn't match")} the file ${coloredFilename}.\n\n${this.utils.diff(
|
||||
content,
|
||||
output,
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (updateSnapshot === "new" || updateSnapshot === "all") {
|
||||
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
||||
fs.writeFileSync(filepath, content);
|
||||
|
||||
this.snapshotState.added++;
|
||||
|
||||
return { pass: true, message: () => "" };
|
||||
} else {
|
||||
this.snapshotState.unmatched++;
|
||||
|
||||
return {
|
||||
pass: true,
|
||||
message: () => `The output file ${coloredFilename} ${errorColor("doesn't exist")}.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect.extend({ toMatchRawFileSnapshot } as any);
|
|
@ -0,0 +1,107 @@
|
|||
!<!CodeModel>
|
||||
info: !<!Info>
|
||||
description: Acceptance test for file with json content type.
|
||||
title: 'Binary with content-type: application/json'
|
||||
schemas: !<!Schemas>
|
||||
strings:
|
||||
- !<!StringSchema> &ref_0
|
||||
type: string
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: string
|
||||
description: simple string
|
||||
protocol: !<!Protocols> {}
|
||||
binaries:
|
||||
- !<!BinarySchema> &ref_1
|
||||
type: binary
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: binary
|
||||
description: ''
|
||||
protocol: !<!Protocols> {}
|
||||
globalParameters:
|
||||
- !<!Parameter> &ref_3
|
||||
schema: *ref_0
|
||||
clientDefaultValue: ''
|
||||
implementation: Client
|
||||
origin: 'modelerfour:synthesized/host'
|
||||
required: true
|
||||
extensions:
|
||||
x-ms-skip-url-encoding: true
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: $host
|
||||
description: server parameter
|
||||
serializedName: $host
|
||||
protocol: !<!Protocols>
|
||||
http: !<!HttpParameter>
|
||||
in: uri
|
||||
operationGroups:
|
||||
- !<!OperationGroup>
|
||||
$key: Upload
|
||||
operations:
|
||||
- !<!Operation>
|
||||
apiVersions:
|
||||
- !<!ApiVersion>
|
||||
version: 1.0.0
|
||||
parameters:
|
||||
- *ref_3
|
||||
requests:
|
||||
- !<!Request>
|
||||
parameters:
|
||||
- !<!Parameter> &ref_2
|
||||
schema: *ref_1
|
||||
implementation: Method
|
||||
required: true
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: data
|
||||
description: Foo bar
|
||||
protocol: !<!Protocols>
|
||||
http: !<!HttpParameter>
|
||||
in: body
|
||||
style: binary
|
||||
signatureParameters:
|
||||
- *ref_2
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: ''
|
||||
description: ''
|
||||
protocol: !<!Protocols>
|
||||
http: !<!HttpBinaryRequest>
|
||||
path: /file
|
||||
method: post
|
||||
binary: true
|
||||
knownMediaType: binary
|
||||
mediaTypes:
|
||||
- application/json
|
||||
uri: '{$host}'
|
||||
signatureParameters: []
|
||||
responses:
|
||||
- !<!Response>
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: ''
|
||||
description: ''
|
||||
protocol: !<!Protocols>
|
||||
http: !<!HttpResponse>
|
||||
statusCodes:
|
||||
- '200'
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: File
|
||||
description: Uploading json file
|
||||
protocol: !<!Protocols> {}
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: Upload
|
||||
description: ''
|
||||
protocol: !<!Protocols> {}
|
||||
security: !<!Security>
|
||||
authenticationRequired: false
|
||||
language: !<!Languages>
|
||||
default:
|
||||
name: ''
|
||||
description: ''
|
||||
protocol: !<!Protocols>
|
||||
http: !<!HttpModel> {}
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Binary with content-type: application/json",
|
||||
"description": "Acceptance test for file with json content type.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/file": {
|
||||
"x-ms-metadata": {
|
||||
"apiVersions": ["1.0.0"]
|
||||
},
|
||||
"post": {
|
||||
"description": "Uploading json file",
|
||||
"operationId": "Upload_File",
|
||||
"requestBody": {
|
||||
"description": "Foo bar",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"format": "file"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Cowbell was added."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { createTestSession } from "../utils";
|
||||
import { ModelerFour } from "../../modeler/modelerfour";
|
||||
import { readdirSync } from "fs";
|
||||
import { serialize } from "@azure-tools/codegen";
|
||||
import { Model } from "@azure-tools/openapi";
|
||||
import { codeModelSchema } from "@azure-tools/codemodel";
|
||||
|
||||
const cfg = {
|
||||
"modelerfour": {
|
||||
"flatten-models": true,
|
||||
"flatten-payloads": true,
|
||||
"group-parameters": true,
|
||||
"resolve-schema-name-collisons": true,
|
||||
"additional-checks": true,
|
||||
//'always-create-content-type-parameter': true,
|
||||
"naming": {
|
||||
override: {
|
||||
$host: "$host",
|
||||
cmyk: "CMYK",
|
||||
},
|
||||
local: "_ + camel",
|
||||
constantParameter: "pascal",
|
||||
/*
|
||||
for when playing with python style settings :
|
||||
|
||||
parameter: 'snakecase',
|
||||
property: 'snakecase',
|
||||
operation: 'snakecase',
|
||||
operationGroup: 'pascalcase',
|
||||
choice: 'pascalcase',
|
||||
choiceValue: 'uppercase',
|
||||
constant: 'uppercase',
|
||||
type: 'pascalcase',
|
||||
// */
|
||||
},
|
||||
},
|
||||
"payload-flattening-threshold": 2,
|
||||
};
|
||||
|
||||
const inputsFolder = `${__dirname}/inputs/`;
|
||||
const expectedFolder = `${__dirname}/expected/`;
|
||||
|
||||
describe("Testing rendering specific scenarios", () => {
|
||||
const folders = readdirSync(inputsFolder);
|
||||
|
||||
for (const folder of folders) {
|
||||
it(`generate model for '${folder}'`, async () => {
|
||||
const session = await createTestSession<Model>(cfg, `${inputsFolder}/${folder}`, ["openapi-document.json"]);
|
||||
|
||||
const modeler = await new ModelerFour(session).init();
|
||||
const codeModel = modeler.process();
|
||||
|
||||
const yaml = serialize(codeModel, codeModelSchema);
|
||||
expect(yaml).toMatchRawFileSnapshot(`${expectedFolder}/head/modeler.yaml`);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
import "./custom-matchers";
|
|
@ -0,0 +1,45 @@
|
|||
import { readFile } from "@azure-tools/async-io";
|
||||
import { deserialize, fail } from "@azure-tools/codegen";
|
||||
import { Session, startSession } from "@azure-tools/autorest-extension-base";
|
||||
|
||||
async function readData(
|
||||
folder: string,
|
||||
...files: Array<string>
|
||||
): Promise<Map<string, { model: any; filename: string; content: string }>> {
|
||||
const results = new Map<string, { model: any; filename: string; content: string }>();
|
||||
|
||||
for (const filename of files) {
|
||||
const content = await readFile(`${folder}/${filename}`);
|
||||
const model = deserialize<any>(content, filename);
|
||||
results.set(filename, {
|
||||
model,
|
||||
filename,
|
||||
content,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function createTestSession<TInputModel>(
|
||||
config: any,
|
||||
folder: string,
|
||||
inputs: Array<string>,
|
||||
): Promise<Session<TInputModel>> {
|
||||
const models = await readData(folder, ...inputs);
|
||||
|
||||
return await startSession<TInputModel>({
|
||||
ReadFile: (filename: string) =>
|
||||
Promise.resolve(models.get(filename)?.content ?? fail(`missing input '${filename}'`)),
|
||||
GetValue: (key: string) => Promise.resolve(key ? config[key] : config),
|
||||
ListInputs: (artifactType?: string) => Promise.resolve([...models.values()].map((x) => x.filename)),
|
||||
ProtectFiles: (path: string) => Promise.resolve(),
|
||||
WriteFile: (filename: string, content: string, sourceMap?: any, artifactType?: string) => Promise.resolve(),
|
||||
Message: (message: any): void => {
|
||||
if (message.Channel === "warning" || message.Channel === "error" || message.Channel === "verbose") {
|
||||
console.error(`${message.Channel} ${message.Text}`);
|
||||
}
|
||||
},
|
||||
UpdateConfigurationFile: (filename: string, content: string) => {},
|
||||
GetConfigurationFile: (filename: string) => Promise.resolve(""),
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./helper";
|
|
@ -3,7 +3,8 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"types": ["mocha"]
|
||||
"types": ["mocha", "jest"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "resources", "node_modules", "**/*.d.ts"]
|
||||
|
|
Загрузка…
Ссылка в новой задаче