diff --git a/specs/ExternalTilesetExtensionsValidationSpec.ts b/specs/ExternalTilesetExtensionsValidationSpec.ts new file mode 100644 index 00000000..29b7efb5 --- /dev/null +++ b/specs/ExternalTilesetExtensionsValidationSpec.ts @@ -0,0 +1,80 @@ +import { Validators } from "../src/validation/Validators"; + +// Note: These test cases use a dummy extension called +// VENDOR_example_extension +// So they will always create at least a WARNING about +// this not being supported + +describe("External tileset extensions validation", function () { + it("detects no errors in declaredInBothContainedInExternal", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInExternal/tileset.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("EXTERNAL_TILESET_VALIDATION_WARNING"); + expect(result.get(0).causes[0].type).toEqual("EXTENSION_NOT_SUPPORTED"); + }); + + it("detects no errors (but one warning) in declaredInBothContainedInTileset", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInTileset/tileset.json" + ); + expect(result.length).toEqual(2); + expect(result.get(0).type).toEqual("EXTENSION_NOT_SUPPORTED"); + expect(result.get(1).type).toEqual("EXTERNAL_TILESET_VALIDATION_WARNING"); + expect(result.get(1).causes[0].type).toEqual( + "EXTENSION_USED_BUT_NOT_FOUND" + ); + }); + + it("detects an error in declaredInExternalContainedInExternal", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInExternal/tileset.json" + ); + expect(result.length).toEqual(2); + expect(result.get(0).type).toEqual("EXTERNAL_TILESET_VALIDATION_WARNING"); + expect(result.get(0).causes[0].type).toEqual("EXTENSION_NOT_SUPPORTED"); + expect(result.get(1).type).toEqual("EXTENSION_FOUND_BUT_NOT_USED"); + }); + + it("detects an error and a warning in declaredInExternalContainedInTileset", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInTileset/tileset.json" + ); + expect(result.length).toEqual(3); + expect(result.get(0).type).toEqual("EXTENSION_NOT_SUPPORTED"); + expect(result.get(1).type).toEqual("EXTERNAL_TILESET_VALIDATION_WARNING"); + expect(result.get(1).causes[0].type).toEqual( + "EXTENSION_USED_BUT_NOT_FOUND" + ); + expect(result.get(2).type).toEqual("EXTENSION_FOUND_BUT_NOT_USED"); + }); + + it("detects two errors in declaredInNoneContainedInExternal", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/externalTilesetExtensions/declaredInNoneContainedInExternal/tileset.json" + ); + expect(result.length).toEqual(2); + expect(result.get(0).type).toEqual("EXTERNAL_TILESET_VALIDATION_ERROR"); + expect(result.get(1).type).toEqual("EXTENSION_FOUND_BUT_NOT_USED"); + }); + + it("detects one error in declaredInTilesetContainedInExternal", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInExternal/tileset.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("EXTERNAL_TILESET_VALIDATION_ERROR"); + expect(result.get(0).causes[1].type).toEqual( + "EXTENSION_FOUND_BUT_NOT_USED" + ); + }); + + it("detects no errors in declaredInTilesetContainedInTileset", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInTileset/tileset.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("EXTENSION_NOT_SUPPORTED"); + }); +}); diff --git a/specs/data/tilesets/externalTilesetExtensions/README.md b/specs/data/tilesets/externalTilesetExtensions/README.md new file mode 100644 index 00000000..4f31c793 --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/README.md @@ -0,0 +1,25 @@ + +Test data for the validation of extensions in the context of external tilesets. + +(This is a special aspect of https://github.com/CesiumGS/3d-tiles-validator/issues/231 ). + +Each of these data sets consists of a `tileset.json` that refers to an `external.json`. + +They use a "dummy" extension called `VENDOR_example_extension` to check the validation +of the `extensionsUsed` propery of a tileset. (This means that the validation will +always at least create one warning, because the extension is not supported). + +In the following, the term.. + +- 'declared' means that a tileset declares the extension in its `extesionsUsed` +- 'contained' means that the tileset actually contains such an extension object in its `extensions` + +These cases are considered: + +- `declaredInBothContainedInExternal`: This is valid. The external tileset contains it and declares it. The main tileset also declares it (because it refers to one that contains it). +- `declaredInBothContainedInTileset`: This valid, but causes a warning. The external tileset does not have to declare it if it does not contain it. +- `declaredInExternalContainedInExternal`: This is invalid. The main tileset also has to declare it when it refers to one that contains it. +- `declaredInExternalContainedInTileset`: This is invalid. The main tileset has to declare it if it contains it. +- `declaredInNoneContainedInExternal`: This is invalid. Both tilesets have to declare it when the external one contains it. +- `declaredInTilesetContainedInExternal`: This is invalid. The external one also has to declare it when it contains it +- `declaredInTilesetContainedInTileset`: This is valid. Only the main tileset has to declare it if only the main tileset contains it. diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInExternal/external.json b/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInExternal/external.json new file mode 100644 index 00000000..c1b2c3a2 --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInExternal/external.json @@ -0,0 +1,23 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0 + }, + "extensionsUsed": [ + "VENDOR_example_extension" + ], + "extensions": { + "VENDOR_example_extension": { + "exampleExtension": { + "description": "A dummy example extension" + } + } + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInExternal/tileset.json b/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInExternal/tileset.json new file mode 100644 index 00000000..f31ab3ac --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInExternal/tileset.json @@ -0,0 +1,27 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0, + "children": [ + { + "geometricError" : 1.0, + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "external.json" + } + } + ] + }, + "extensionsUsed": [ + "VENDOR_example_extension" + ] +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInTileset/external.json b/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInTileset/external.json new file mode 100644 index 00000000..3c8340cb --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInTileset/external.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0 + }, + "extensionsUsed": [ + "VENDOR_example_extension" + ] +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInTileset/tileset.json b/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInTileset/tileset.json new file mode 100644 index 00000000..908cda72 --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInBothContainedInTileset/tileset.json @@ -0,0 +1,34 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0, + "children": [ + { + "geometricError" : 1.0, + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "external.json" + } + } + ] + }, + "extensionsUsed": [ + "VENDOR_example_extension" + ], + "extensions": { + "VENDOR_example_extension": { + "exampleExtension": { + "description": "A dummy example extension" + } + } + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInExternal/external.json b/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInExternal/external.json new file mode 100644 index 00000000..c1b2c3a2 --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInExternal/external.json @@ -0,0 +1,23 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0 + }, + "extensionsUsed": [ + "VENDOR_example_extension" + ], + "extensions": { + "VENDOR_example_extension": { + "exampleExtension": { + "description": "A dummy example extension" + } + } + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInExternal/tileset.json b/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInExternal/tileset.json new file mode 100644 index 00000000..907e9e1e --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInExternal/tileset.json @@ -0,0 +1,24 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0, + "children": [ + { + "geometricError" : 1.0, + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "external.json" + } + } + ] + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInTileset/external.json b/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInTileset/external.json new file mode 100644 index 00000000..3c8340cb --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInTileset/external.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0 + }, + "extensionsUsed": [ + "VENDOR_example_extension" + ] +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInTileset/tileset.json b/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInTileset/tileset.json new file mode 100644 index 00000000..e8cf9aa4 --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInExternalContainedInTileset/tileset.json @@ -0,0 +1,31 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0, + "children": [ + { + "geometricError" : 1.0, + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "external.json" + } + } + ] + }, + "extensions": { + "VENDOR_example_extension": { + "exampleExtension": { + "description": "A dummy example extension" + } + } + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInNoneContainedInExternal/external.json b/specs/data/tilesets/externalTilesetExtensions/declaredInNoneContainedInExternal/external.json new file mode 100644 index 00000000..7a808613 --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInNoneContainedInExternal/external.json @@ -0,0 +1,20 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0 + }, + "extensions": { + "VENDOR_example_extension": { + "exampleExtension": { + "description": "A dummy example extension" + } + } + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInNoneContainedInExternal/tileset.json b/specs/data/tilesets/externalTilesetExtensions/declaredInNoneContainedInExternal/tileset.json new file mode 100644 index 00000000..907e9e1e --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInNoneContainedInExternal/tileset.json @@ -0,0 +1,24 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0, + "children": [ + { + "geometricError" : 1.0, + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "external.json" + } + } + ] + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInExternal/external.json b/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInExternal/external.json new file mode 100644 index 00000000..7a808613 --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInExternal/external.json @@ -0,0 +1,20 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0 + }, + "extensions": { + "VENDOR_example_extension": { + "exampleExtension": { + "description": "A dummy example extension" + } + } + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInExternal/tileset.json b/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInExternal/tileset.json new file mode 100644 index 00000000..f31ab3ac --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInExternal/tileset.json @@ -0,0 +1,27 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0, + "children": [ + { + "geometricError" : 1.0, + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "external.json" + } + } + ] + }, + "extensionsUsed": [ + "VENDOR_example_extension" + ] +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInTileset/external.json b/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInTileset/external.json new file mode 100644 index 00000000..eba430ef --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInTileset/external.json @@ -0,0 +1,13 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0 + } +} diff --git a/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInTileset/tileset.json b/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInTileset/tileset.json new file mode 100644 index 00000000..908cda72 --- /dev/null +++ b/specs/data/tilesets/externalTilesetExtensions/declaredInTilesetContainedInTileset/tileset.json @@ -0,0 +1,34 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 4.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 2.0, + "children": [ + { + "geometricError" : 1.0, + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "external.json" + } + } + ] + }, + "extensionsUsed": [ + "VENDOR_example_extension" + ], + "extensions": { + "VENDOR_example_extension": { + "exampleExtension": { + "description": "A dummy example extension" + } + } + } +} diff --git a/src/validation/ContentDataValidator.ts b/src/validation/ContentDataValidator.ts index e645e10c..eadfa128 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -166,6 +166,14 @@ export class ContentDataValidator { ); const derivedResult = derivedContext.getResult(); + // Add all extensions that have been found in the content + // to the current context. They also have to appear in + // the 'extensionsUsed' of the containing tileset. + const derivedExtensionsFound = derivedContext.getExtensionsFound(); + for (const derivedExtensionFound of derivedExtensionsFound) { + context.addExtensionFound(derivedExtensionFound); + } + if (isTileset) { const issue = ContentValidationIssues.createForExternalTileset( contentUri, diff --git a/src/validation/TilesetPackageValidator.ts b/src/validation/TilesetPackageValidator.ts index 36141733..cbdb4aa1 100644 --- a/src/validation/TilesetPackageValidator.ts +++ b/src/validation/TilesetPackageValidator.ts @@ -239,6 +239,14 @@ export class TilesetPackageValidator implements Validator { ); const derivedResult = derivedContext.getResult(); + // Add all extensions that have been found in the tileset + // to the current context. They also have to appear in + // the 'extensionsUsed' of the containing tileset. + const derivedExtensionsFound = derivedContext.getExtensionsFound(); + for (const derivedExtensionFound of derivedExtensionsFound) { + context.addExtensionFound(derivedExtensionFound); + } + if (isContent) { const issue = ContentValidationIssues.createForContent( uri, diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index 9713defa..12abbe34 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -84,6 +84,14 @@ export class ValidationContext { * against the original `ResourceResolver`, yielding one that * resolves resources against the resulting path. * + * The returned context will initially not have any records of + * extensions that are 'found' (i.e. `getExtensionsFound` will + * be empty). Depending on the purpose of the derived context, + * and details about the validation of 'used' extensions + * (see https://github.com/CesiumGS/3d-tiles-validator/issues/231 ), + * the caller may decide to add the `getExtensionsUsed` of the + * derived context to the context that it was derived from. + * * @param uri - The (usually relative) URI * @returns The new instance */ @@ -95,7 +103,7 @@ export class ValidationContext { derivedResourceResolver, this._options ); - derived._extensionsFound = this._extensionsFound; + derived._extensionsFound = new Set(); derived._activeTilesetUris = this._activeTilesetUris; return derived; } @@ -106,7 +114,15 @@ export class ValidationContext { * It uses the same `ValidationOptions` as this one, with * a base URI that is derived by resolving the given URI * against the current base URI, and uses the given - * `ResourceResolver` + * `ResourceResolver`. + * + * The returned context will initially not have any records of + * extensions that are 'found' (i.e. `getExtensionsFound` will + * be empty). Depending on the purpose of the derived context, + * and details about the validation of 'used' extensions + * (see https://github.com/CesiumGS/3d-tiles-validator/issues/231 ), + * the caller may decide to add the `getExtensionsUsed` of the + * derived context to the context that it was derived from. * * @param uri - The (usually relative) URI * @param resourceResolver - The resource resolver @@ -122,7 +138,7 @@ export class ValidationContext { resourceResolver, this._options ); - derived._extensionsFound = this._extensionsFound; + derived._extensionsFound = new Set(); derived._activeTilesetUris = this._activeTilesetUris; return derived; }