зеркало из
1
0
Форкнуть 0

Internal: New test setup using snapshots to validate produced models (#365)

This commit is contained in:
Timothee Guerin 2020-12-04 11:56:09 -08:00 коммит произвёл GitHub
Родитель 8b75335858
Коммит b95da6bb05
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 400 добавлений и 21 удалений

Просмотреть файл

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

Просмотреть файл

@ -2144,7 +2144,7 @@ export class ModelerFour {
}
const kmtBinary = groupedMediaTypes.get(KnownMediaType.Binary);
if (kmtBinary) {
// handle binary
this.processBinary(KnownMediaType.Binary, kmtBinary, operation, requestBody);
@ -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"]