quicktype/test/test.ts

507 строки
14 KiB
TypeScript
Executable File

import * as process from "process";
import * as os from "os";
import * as fs from "fs";
import * as path from "path";
import * as _ from "lodash";
import { main as quicktype_, Options } from "../cli/quicktype";
import { inParallel } from "./lib/multicore";
import deepEquals from "./lib/deepEquals";
import { randomBytes } from "crypto";
const Ajv = require('ajv');
const strictDeepEquals: (x: any, y: any) => boolean = require('deep-equal');
const shell = require("shelljs");
const Main = require("../output/Main");
const Samples = require("../output/Samples");
const exit = require('exit');
const chalk = require("chalk");
//////////////////////////////////////
// Constants
/////////////////////////////////////
const IS_CI = process.env.CI === "true";
const BRANCH = process.env.TRAVIS_BRANCH;
const IS_BLESSED = ["master"].indexOf(BRANCH) !== -1;
const IS_PUSH = process.env.TRAVIS_EVENT_TYPE === "push";
const IS_PR = process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== "false";
const DEBUG = typeof process.env.DEBUG !== 'undefined';
const CPUs = +process.env.CPUs || os.cpus().length;
function debug<T>(x: T): T {
if (DEBUG) console.log(x);
return x;
}
//////////////////////////////////////
// Fixtures
/////////////////////////////////////
interface Fixture {
name: string;
base: string;
setup?: string;
diffViaSchema: boolean;
output: string;
topLevel: string;
skip?: string[]
test(sample: string): Promise<void>;
}
const FIXTURES: Fixture[] = [
{
name: "csharp",
base: "test/fixtures/csharp",
// https://github.com/dotnet/cli/issues/1582
setup: "dotnet restore --no-cache",
diffViaSchema: true,
output: "QuickType.cs",
topLevel: "QuickType",
test: testCSharp
},
{
name: "java",
base: "test/fixtures/java",
diffViaSchema: false,
output: "src/main/java/io/quicktype/TopLevel.java",
topLevel: "TopLevel",
test: testJava,
skip: [
"identifiers.json",
"simple-identifiers.json",
"blns-object.json"
]
},
{
name: "golang",
base: "test/fixtures/golang",
diffViaSchema: true,
output: "quicktype.go",
topLevel: "TopLevel",
test: testGo,
skip: [
"identifiers.json",
"simple-identifiers.json",
"blns-object.json"
]
},
{
name: "schema",
base: "test/fixtures/golang",
diffViaSchema: false,
output: "schema.json",
topLevel: "schema",
test: testJsonSchema,
skip: [
"identifiers.json",
"simple-identifiers.json",
"blns-object.json"
]
},
{
name: "elm",
base: "test/fixtures/elm",
setup: "rm -rf elm-stuff/build-artifacts && elm-make --yes",
diffViaSchema: true,
output: "QuickType.elm",
topLevel: "QuickType",
test: testElm,
skip: [
"identifiers.json",
"simple-identifiers.json",
"blns-object.json"
]
},
{
name: "swift",
base: "test/fixtures/swift",
diffViaSchema: false,
output: "quicktype.swift",
topLevel: "TopLevel",
test: testSwift,
skip: [
"identifiers.json",
"no-classes.json",
"blns-object.json"
]
},
{
name: "typescript",
base: "test/fixtures/typescript",
diffViaSchema: true,
output: "TopLevel.ts",
topLevel: "TopLevel",
test: testTypeScript,
skip: [
"identifiers.json"
]
}
].filter(({name}) => !process.env.FIXTURE || process.env.FIXTURE.includes(name));
//////////////////////////////////////
// Go tests
/////////////////////////////////////
async function testGo(sample: string) {
compareJsonFileToJson({
expectedFile: sample,
jsonCommand: `go run main.go quicktype.go < "${sample}"`,
strict: false
});
}
//////////////////////////////////////
// C# tests
/////////////////////////////////////
async function testCSharp(sample: string) {
compareJsonFileToJson({
expectedFile: sample,
jsonCommand: `dotnet run "${sample}"`,
strict: false
});
}
//////////////////////////////////////
// Java tests
/////////////////////////////////////
async function testJava(sample: string) {
exec(`mvn package`);
compareJsonFileToJson({
expectedFile: sample,
jsonCommand: `java -cp target/QuickTypeTest-1.0-SNAPSHOT.jar io.quicktype.App "${sample}"`,
strict: false
});
}
//////////////////////////////////////
// Elm tests
/////////////////////////////////////
async function testElm(sample: string) {
exec(`elm-make Main.elm QuickType.elm --output elm.js`);
compareJsonFileToJson({
expectedFile: sample,
jsonCommand: `node ./runner.js "${sample}"`,
strict: false
});
}
//////////////////////////////////////
// Swift tests
/////////////////////////////////////
async function testSwift(sample: string) {
exec(`swiftc -o quicktype main.swift quicktype.swift`);
compareJsonFileToJson({
expectedFile: sample,
jsonCommand: `./quicktype "${sample}"`,
strict: false
});
}
//////////////////////////////////////
// JSON Schema tests
/////////////////////////////////////
async function testJsonSchema(sample: string) {
let input = JSON.parse(fs.readFileSync(sample, "utf8"));
let schema = JSON.parse(fs.readFileSync("schema.json", "utf8"));
let ajv = new Ajv();
let valid = ajv.validate(schema, input);
if (!valid) {
failWith("Generated schema does not validate input JSON.", {
sample
});
}
// Generate Go from the schema
await quicktype({ src: ["schema.json"], srcLang: "schema", out: "quicktype.go", topLevel: "TopLevel" });
// Parse the sample with Go generated from its schema, and compare to the sample
compareJsonFileToJson({
expectedFile: sample,
jsonCommand: `go run main.go quicktype.go < "${sample}"`,
strict: false
});
// Generate a schema from the schema, making sure the schemas are the same
let schemaSchema = "schema-from-schema.json";
await quicktype({ src: ["schema.json"], srcLang: "schema", lang: "schema", out: schemaSchema });
compareJsonFileToJson({
expectedFile: "schema.json",
jsonFile: schemaSchema,
strict: true
});
}
//////////////////////////////////////
// TypeScript test
/////////////////////////////////////
async function testTypeScript(sample) {
compareJsonFileToJson({
expectedFile: sample,
// We have to unset TS_NODE_PROJECT because it gets set on the workers
// to the root test/tsconfig.json
jsonCommand: `TS_NODE_PROJECT= ts-node main.ts \"${sample}\"`,
strict: false
});
}
//////////////////////////////////////
// Test driver
/////////////////////////////////////
function failWith(message: string, obj: any) {
obj.cwd = process.cwd();
console.error(chalk.red(message));
console.error(chalk.red(JSON.stringify(obj, null, " ")));
throw obj;
}
async function time<T>(work: () => Promise<T>): Promise<[T, number]> {
let start = +new Date();
let result = await work();
let end = +new Date();
return [result, end - start];
}
async function quicktype(opts: Options) {
let [_, duration] = await time(async () => {
await quicktype_(opts);
});
}
function exec(
s: string,
opts: { silent: boolean } = { silent: !DEBUG },
cb?: any)
: { stdout: string; code: number; } {
debug(s);
let result = shell.exec(s, opts, cb);
if (result.code !== 0) {
console.error(result.stdout);
console.error(result.stderr);
failWith("Command failed", {
command: s,
code: result.code
});
}
return result;
}
function callAndReportFailure<T>(message: string, f: () => T): T {
try {
return f();
} catch (e) {
failWith(message, { error: e });
}
}
type ComparisonArgs = {
expectedFile: string;
jsonFile?: string;
jsonCommand?: string;
strict: boolean
};
function compareJsonFileToJson(args: ComparisonArgs) {
debug(args);
let { expectedFile, jsonFile, jsonCommand, strict } = args;
const jsonString = jsonFile
? callAndReportFailure("Could not read JSON output file", () => fs.readFileSync(jsonFile, "utf8"))
: callAndReportFailure("Could not run command for JSON output", () => exec(jsonCommand).stdout);
const givenJSON = callAndReportFailure("Could not parse output JSON", () => JSON.parse(jsonString));
const expectedJSON = callAndReportFailure("Could not read or parse expected JSON file",
() => JSON.parse(fs.readFileSync(expectedFile, "utf8")));
let jsonAreEqual = strict
? callAndReportFailure("Failed to strictly compare objects", () => strictDeepEquals(givenJSON, expectedJSON))
: callAndReportFailure("Failed to compare objects.", () => deepEquals(expectedJSON, givenJSON));
if (!jsonAreEqual) {
failWith("Error: Output is not equivalent to input.", {
expectedFile,
jsonCommand,
jsonFile
});
}
}
async function inDir(dir: string, work: () => Promise<void>) {
let origin = process.cwd();
debug(`cd ${dir}`)
process.chdir(dir);
await work();
process.chdir(origin);
}
function shouldSkipTest(fixture: Fixture, sample: string): boolean {
if (fs.statSync(sample).size > 32 * 1024 * 1024) {
return true;
}
let skips = fixture.skip || [];
return _.includes(skips, path.basename(sample));
}
async function runFixtureWithSample(fixture: Fixture, sample: string, index: number, total: number) {
let cwd = `test/runs/${fixture.name}-${randomBytes(3).toString('hex')}`;
let sampleFile = path.basename(sample);
let shouldSkip = shouldSkipTest(fixture, sample);
console.error(
`*`,
chalk.dim(`[${index+1}/${total}]`),
chalk.magenta(fixture.name),
path.join(
cwd,
chalk.cyan(path.basename(sample))),
shouldSkip
? chalk.red("SKIP")
: '');
if (shouldSkip) return;
shell.cp("-R", fixture.base, cwd);
shell.cp(sample, cwd);
await inDir(cwd, async () => {
// Generate code from the sample
await quicktype({ src: [sampleFile], out: fixture.output, topLevel: fixture.topLevel});
try {
await fixture.test(sampleFile);
} catch (e) {
failWith("Fixture threw an exception", { error: e });
}
if (fixture.diffViaSchema) {
debug("* Diffing with code generated via JSON Schema");
// Make a schema
await quicktype({ src: [sampleFile], out: "schema.json", topLevel: fixture.topLevel});
// Quicktype from the schema and compare to expected code
shell.mv(fixture.output, `${fixture.output}.expected`);
await quicktype({ src: ["schema.json"], srcLang: "schema", out: fixture.output, topLevel: fixture.topLevel});
// Compare fixture.output to fixture.output.expected
exec(`diff -Naur ${fixture.output}.expected ${fixture.output} > /dev/null 2>&1`);
}
});
shell.rm("-rf", cwd);
}
type WorkItem = { sample: string; fixtureName: string; }
async function testAll(samples: string[]) {
// Get an array of all { sample, fixtureName } objects we'll run
let tests = _
.chain(samples)
.flatMap(sample => FIXTURES.map(fixture => {
return { sample, fixtureName: fixture.name };
}))
.value();
await inParallel({
queue: tests,
workers: CPUs,
setup: async () => {
testCLI();
console.error(`* Running ${samples.length} tests on ${FIXTURES.length} fixtures`);
for (let { name, base, setup } of FIXTURES) {
exec(`rm -rf test/runs`);
exec(`mkdir -p test/runs`);
if (setup) {
console.error(
`* Setting up`,
chalk.magenta(name),
`fixture`);
await inDir(base, async () => { exec(setup); });
}
}
},
map: async ({ sample, fixtureName }: WorkItem, index) => {
let fixture = _.find(FIXTURES, { name: fixtureName });
try {
await runFixtureWithSample(fixture, sample, index, tests.length);
} catch (e) {
console.trace(e);
exit(1);
}
}
});
}
function testCLI() {
console.log(`* CLI sanity check`);
const qt = "node output/quicktype.js";
exec(`${qt} --help`);
}
function testsInDir(dir: string): string[] {
return shell.ls(`${dir}/*.json`);
}
async function main(sources: string[]) {
let prioritySources = _.concat(
testsInDir("test/inputs/json/priority"),
testsInDir("test/inputs/json/samples"),
);
let miscSources = testsInDir("test/inputs/json/misc");
if (sources.length == 0) {
sources = _.concat(
prioritySources,
_.shuffle(miscSources)
);
} else if (sources.length == 1 && fs.lstatSync(sources[0]).isDirectory()) {
sources = testsInDir(sources[0]);
}
if (IS_CI && !IS_PR && !IS_BLESSED) {
// Run only priority sources on low-priority CI branches
sources = prioritySources;
} else if (IS_CI) {
// On CI, we run a maximum number of test samples. First we test
// the priority samples to fail faster, then we continue testing
// until testMax with random sources.
let testMax = 100;
sources = _.concat(
prioritySources,
_.chain(miscSources).shuffle().take(testMax - prioritySources.length).value()
);
}
await testAll(sources);
}
// skip 2 `node` args
main(process.argv.slice(2)).catch(reason => {
console.error(reason);
process.exit(1);
});