/* * 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 . */ 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); } }