From f2a86fb6bd93258dd10056eb1b791fd5aaf83fe5 Mon Sep 17 00:00:00 2001 From: Karl-Philipp Wulfert Date: Tue, 14 May 2019 19:18:19 +0200 Subject: [PATCH] test: add tests to ensure consistency of model --- test/Schema.spec.ts | 245 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 4 deletions(-) diff --git a/test/Schema.spec.ts b/test/Schema.spec.ts index be690235..aa9d2db5 100644 --- a/test/Schema.spec.ts +++ b/test/Schema.spec.ts @@ -12,24 +12,261 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +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 'mocha-typescript'; import {join, resolve} from 'path'; - -const logger = new Logger(); +import {DeclarationReflection, ProjectReflection} from 'typedoc'; +import {ArrayType, IntrinsicType, ReferenceType, StringLiteralType, Type, UnionType} from 'typedoc/dist/lib/models'; process.on('unhandledRejection', (err) => { logger.error('UNHANDLED REJECTION', err.stack); process.exit(1); }); +const logger = new Logger(); + +/** + * 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 ValidateTestFiles { +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 - async validateTestFiles() { + '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; + } + + const 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)) { + expect(SchemaSpec.thingNames).not.to.contain( + type.name, + `Property '${property.name}' on type '${thingName}' has element type '${type.name}'.`, + ); + } 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;