Files
openstapps/test/schema.spec.ts
2020-10-30 09:50:46 +01:00

297 lines
9.4 KiB
TypeScript

/*
* Copyright (C) 2018 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {getProjectReflection} from '@openstapps/core-tools/lib/common';
import {validateFiles, writeReport} from '@openstapps/core-tools/lib/validate';
import {Logger} from '@openstapps/logger';
import {fail} from 'assert';
import {expect} from 'chai';
import {mkdirSync} from 'fs';
import {slow, suite, test, timeout} from '@testdeck/mocha';
import {join, resolve} from 'path';
import {DeclarationReflection, ProjectReflection} from 'typedoc';
import {ArrayType, IntrinsicType, ReferenceType, StringLiteralType, Type, UnionType} from 'typedoc/dist/lib/models';
process.on('unhandledRejection', (err) => {
throw err;
});
/**
* Check if type is a union type
*
* @param type Type to check
*/
function isUnionType(type: Type): type is UnionType {
return type.type === 'union';
}
/**
* Check if a type is reference type
*
* @param type Type to check
*/
function isReferenceType(type: Type): type is ReferenceType {
return type.type === 'reference';
}
/**
* Check if a type is an array type
*
* @param type Type to check
*/
function isArrayType(type: Type): type is ArrayType {
return type.type === 'array';
}
/**
* Check if a type is an intrinsic type
*
* @param type Type to check
*/
function isIntrinsicType(type: Type): type is IntrinsicType {
return type.type === 'intrinsic';
}
/**
* Check if a type is a string literal type
*
* @param type Type to check
*/
function isStringLiteralType(type: Type): type is StringLiteralType {
return type.type === 'stringLiteral';
}
/**
* Get extended types of a declaration reflection
* @param thingReflection Reflection of the thing
* @param objects Map of reflections by name
*/
function getExtendedTypes(thingReflection: DeclarationReflection,
objects: { [name: string]: DeclarationReflection }): string[] {
const extendedTypes: string[] = [];
if (Array.isArray(thingReflection.extendedTypes)) {
const typesToCheck = thingReflection.extendedTypes.slice();
while (typesToCheck.length > 0) {
const extendedType = typesToCheck.splice(0, 1)[0];
extendedTypes.push((extendedType as unknown as ReferenceType).name);
const extendedObject = objects[(extendedType as unknown as ReferenceType).name];
if (typeof extendedObject !== 'undefined') {
if (Array.isArray(extendedObject.extendedTypes)) {
typesToCheck.push.apply(typesToCheck, extendedObject.extendedTypes);
}
}
}
}
return extendedTypes;
}
@suite(timeout(10000), slow(5000))
export class SchemaSpec {
static objects: { [name: string]: DeclarationReflection } = {};
static reflection: ProjectReflection;
static thingNames: string[];
static before() {
SchemaSpec.reflection = getProjectReflection(resolve(__dirname, '..', 'src'));
if (Array.isArray(SchemaSpec.reflection.children)) {
for (const module of SchemaSpec.reflection.children) {
if (Array.isArray(module.children)) {
for (const object of module.children) {
SchemaSpec.objects[object.name] = object;
}
}
}
}
const thingsReflection = SchemaSpec.objects.SCThingsWithoutDiff;
// tslint:disable-next-line:no-unused-expression
expect(thingsReflection).not.to.be.undefined;
// tslint:disable-next-line:no-unused-expression
expect(isUnionType(thingsReflection.type!)).to.be.true;
(thingsReflection.type! as UnionType).types.push({
'id': 0,
'name': 'SCDiff',
'type': 'reference',
} as unknown as ReferenceType);
// tslint:disable-next-line:no-unused-expression
expect((thingsReflection.type! as UnionType).types.every(isReferenceType)).to.be.true;
SchemaSpec.thingNames = (thingsReflection.type! as UnionType).types.map((type) => {
return (type as ReferenceType).name;
});
}
@test
'all things have an origin'() {
for (const thingName of SchemaSpec.thingNames) {
const thingReflection = SchemaSpec.objects[`${thingName}`];
let originFound = false;
if (Array.isArray(thingReflection.children)) {
for (const property of thingReflection.children) {
if (property.name === 'origin') {
originFound = true;
break;
}
}
}
// tslint:disable-next-line:no-unused-expression
expect(originFound).to.be.equal(true, `'${thingName}' must have property 'origin'.`);
}
}
@test
'does not have duplicate names'() {
const names: string[] = [];
if (Array.isArray(SchemaSpec.reflection.children)) {
for (const module of SchemaSpec.reflection.children) {
if (Array.isArray(module.children)) {
for (const object of module.children) {
expect(names).not.to.contain(object.name);
names.push(object.name);
}
}
}
}
}
@test
'no property is an SCThing'() {
for (const thingName of SchemaSpec.thingNames) {
const thingReflection = SchemaSpec.objects[`${thingName}`];
if (Array.isArray(thingReflection.children)) {
for (const property of thingReflection.children) {
if (typeof property.type === 'undefined') {
Logger.error(thingName, property.name);
continue;
}
let type = property.type!;
if (isIntrinsicType(type)) {
continue;
} else if (isArrayType(type)) {
const elementType = type.elementType;
if (isIntrinsicType(elementType)) {
continue;
} else if (isReferenceType(elementType)) {
expect(SchemaSpec.thingNames).not.to.contain(
elementType.name,
`Array property '${property.name}' on type '${thingName}' has element type '${elementType.name}'.`,
);
} else {
// tslint:disable-next-line:max-line-length
fail(`'${thingName}'#'${property.name}' element type '${elementType.type}' is not handled by this test!`);
}
} else if (isReferenceType(type)) {
do {
expect(SchemaSpec.thingNames).not.to.contain(
type.name,
`Property '${property.name}' on type '${thingName}' has element type '${type.name}'.`,
);
const referencedObject = SchemaSpec.objects[type.name];
if (typeof referencedObject !== 'undefined') {
const referencedType = referencedObject.type;
if (typeof referencedType !== 'undefined') {
type = referencedType;
} else {
break;
}
} else {
break;
}
} while (isReferenceType(type));
} else if (isUnionType(type)) {
for (const nestedType of type.types) {
if (isIntrinsicType(nestedType) || isStringLiteralType(nestedType)) {
continue;
} else if (isReferenceType(nestedType)) {
expect(SchemaSpec.thingNames).not.to.contain(
nestedType.name,
`Union property '${property.name}' on type '${thingName}' contains type '${nestedType.name}'.`,
);
} else {
// tslint:disable-next-line:max-line-length
fail(`'${thingName}'#'${property.name}' union type '${nestedType.type}' is not handled by this test!`);
}
}
} else {
// tslint:disable-next-line:max-line-length
fail(`'${thingName}'#'${property.name}' with type '${type.type}' is not handled by this test!`);
}
}
}
}
}
@test
'things extend SCThing'() {
for (const thingName of SchemaSpec.thingNames) {
const thingReflection = SchemaSpec.objects[`${thingName}`];
expect(getExtendedTypes(thingReflection, SchemaSpec.objects)).to.contain(
'SCThing',
`'${thingName}' neither extends 'SCThing' transitively nor directly.`,
);
}
}
@test
'things without references do not extend SCThing'() {
for (const thingName of SchemaSpec.thingNames) {
const thingWithoutReferencesReflection = SchemaSpec.objects[`${thingName}WithoutReferences`];
expect(getExtendedTypes(thingWithoutReferencesReflection, SchemaSpec.objects)).not.to.contain(
'SCThing',
`'${thingName}WithoutReferences' extends 'SCThing' either transitively or directly.`,
);
}
}
@test
async 'validate against test files'() {
const errorsPerFile = await validateFiles(resolve('lib', 'schema'), resolve('test', 'resources'));
let unexpected = false;
Object.keys(errorsPerFile).forEach((file) => {
unexpected = unexpected || errorsPerFile[file].some((error) => !error.expected);
});
mkdirSync('report', {
recursive: true,
});
await writeReport(join('report', 'index.html'), errorsPerFile);
expect(unexpected).to.be.equal(false);
}
}