diff --git a/Core/tests/PlatformMetadataIndex.cpp b/Core/tests/PlatformMetadataIndex.cpp new file mode 100644 index 0000000000..cb27adc099 --- /dev/null +++ b/Core/tests/PlatformMetadataIndex.cpp @@ -0,0 +1,114 @@ +/* + * GDevelop Core + * Copyright 2008-2026 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +/** + * @file Tests for gd::MetadataProvider lookups, which are backed by + * gd::PlatformMetadataIndex (constant-time index of a platform's metadata). + */ +#include "DummyPlatform.h" +#include "GDCore/Extensions/Metadata/BehaviorMetadata.h" +#include "GDCore/Extensions/Metadata/EffectMetadata.h" +#include "GDCore/Extensions/Metadata/ExpressionMetadata.h" +#include "GDCore/Extensions/Metadata/InstructionMetadata.h" +#include "GDCore/Extensions/Metadata/MetadataProvider.h" +#include "GDCore/Extensions/Metadata/ObjectMetadata.h" +#include "GDCore/Extensions/Platform.h" +#include "GDCore/Extensions/PlatformExtension.h" +#include "GDCore/Project/Project.h" +#include "catch.hpp" + +using namespace gd; + +TEST_CASE("PlatformMetadataIndex (via MetadataProvider)", "[common]") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + + SECTION("It resolves every kind of metadata declared by an extension") { + REQUIRE_FALSE(MetadataProvider::IsBadObjectMetadata( + MetadataProvider::GetObjectMetadata(platform, "MyExtension::Sprite"))); + REQUIRE_FALSE(MetadataProvider::IsBadBehaviorMetadata( + MetadataProvider::GetBehaviorMetadata(platform, + "MyExtension::MyBehavior"))); + REQUIRE_FALSE(MetadataProvider::IsBadInstructionMetadata( + MetadataProvider::GetActionMetadata(platform, "MyExtension::DoSomething"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetExpressionMetadata(platform, "MyExtension::GetNumber"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetStrExpressionMetadata(platform, "MyExtension::ToString"))); + } + + SECTION("Behavior actions are indexed alongside free/object actions") { + REQUIRE_FALSE(MetadataProvider::IsBadInstructionMetadata( + MetadataProvider::GetActionMetadata( + platform, "MyExtension::BehaviorDoSomething"))); + } + + SECTION("It resolves object/behavior expressions on their own type") { + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetObjectExpressionMetadata( + platform, "MyExtension::Sprite", "GetObjectNumber"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetObjectStrExpressionMetadata( + platform, "MyExtension::Sprite", "GetObjectStringWith1Param"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetBehaviorExpressionMetadata( + platform, "MyExtension::MyBehavior", "GetBehaviorNumberWith1Param"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetBehaviorStrExpressionMetadata( + platform, "MyExtension::MyBehavior", "GetBehaviorStringWith1Param"))); + } + + SECTION("Object expressions fall back to the base object type") { + // "GetFromBaseExpression" is declared on the base object (""), not on + // Sprite, so it must be resolved through the base-type fallback. + const auto& fromBase = MetadataProvider::GetObjectExpressionMetadata( + platform, "MyExtension::Sprite", "GetFromBaseExpression"); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata(fromBase)); + + // It must also be found for an object type that is not declared by any + // extension (still resolved via the base object). + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetObjectExpressionMetadata( + platform, "UnknownObjectType", "GetFromBaseExpression"))); + } + + SECTION("Unknown types resolve to the bad metadata") { + REQUIRE(MetadataProvider::IsBadObjectMetadata( + MetadataProvider::GetObjectMetadata(platform, "MyExtension::DoesNotExist"))); + REQUIRE(MetadataProvider::IsBadBehaviorMetadata( + MetadataProvider::GetBehaviorMetadata(platform, "Does::NotExist"))); + REQUIRE(MetadataProvider::IsBadInstructionMetadata( + MetadataProvider::GetActionMetadata(platform, "Does::NotExist"))); + REQUIRE(MetadataProvider::IsBadInstructionMetadata( + MetadataProvider::GetConditionMetadata(platform, "Does::NotExist"))); + REQUIRE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetExpressionMetadata(platform, "Does::NotExist"))); + REQUIRE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetObjectExpressionMetadata( + platform, "MyExtension::Sprite", "DoesNotExist"))); + } + + SECTION("It returns the extension that declares the metadata") { + auto extensionAndMetadata = + MetadataProvider::GetExtensionAndObjectMetadata(platform, + "MyExtension::Sprite"); + REQUIRE(extensionAndMetadata.GetExtension().GetName() == "MyExtension"); + + auto badExtensionAndMetadata = + MetadataProvider::GetExtensionAndObjectMetadata(platform, + "MyExtension::DoesNotExist"); + REQUIRE(badExtensionAndMetadata.GetExtension().GetName().empty()); + } + + SECTION("The index is rebuilt after the platform's extensions change") { + REQUIRE_FALSE(MetadataProvider::IsBadObjectMetadata( + MetadataProvider::GetObjectMetadata(platform, "MyExtension::Sprite"))); + + platform.RemoveExtension("MyExtension"); + REQUIRE(MetadataProvider::IsBadObjectMetadata( + MetadataProvider::GetObjectMetadata(platform, "MyExtension::Sprite"))); + } +} diff --git a/GDevelop.js/__tests__/MetadataProviderIndex.js b/GDevelop.js/__tests__/MetadataProviderIndex.js index ab29f1ddfd..46f0bdba0a 100644 --- a/GDevelop.js/__tests__/MetadataProviderIndex.js +++ b/GDevelop.js/__tests__/MetadataProviderIndex.js @@ -2,16 +2,28 @@ const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.j // Checks that gd::MetadataProvider lookups (backed by the platform's // gd::PlatformMetadataIndex) stay correct when extensions are added, replaced -// and removed at runtime - i.e. that the index is properly invalidated. -describe('MetadataProvider index invalidation', () => { +// and removed at runtime - i.e. that the index is properly invalidated - and +// that every kind of metadata is indexed and resolved. +describe('MetadataProvider index', () => { let gd = null; beforeAll(async () => { gd = await initializeGDevelopJs(); }); const extensionName = 'MetadataIndexTestExtension'; - const objectType = extensionName + '::IndexTestObject'; - const actionType = extensionName + '::IndexTestAction'; + const prefix = extensionName + '::'; + const objectType = prefix + 'TestObject'; + const behaviorType = prefix + 'TestBehavior'; + const actionType = prefix + 'TestAction'; + const conditionType = prefix + 'TestCondition'; + const expressionType = prefix + 'TestExpression'; + const strExpressionType = prefix + 'TestStrExpression'; + // Object/behavior expression names are not namespaced (they are scoped to the + // object/behavior type they belong to). + const objectExpressionName = 'TestObjectExpression'; + const objectStrExpressionName = 'TestObjectStrExpression'; + const behaviorExpressionName = 'TestBehaviorExpression'; + const behaviorStrExpressionName = 'TestBehaviorStrExpression'; const makeExtension = (objectFullName) => { const extension = new gd.PlatformExtension(); @@ -22,69 +34,92 @@ describe('MetadataProvider index invalidation', () => { 'Author', 'MIT' ); + const objectConfiguration = new gd.ObjectJsImplementation(); - extension.addObject( - 'IndexTestObject', + const objectMetadata = extension.addObject( + 'TestObject', objectFullName, 'A test object', '', objectConfiguration ); - extension.addAction( - 'IndexTestAction', - 'Index test action', - 'Does something', - 'Does something', + objectMetadata.addExpression(objectExpressionName, 'Obj expr', 'd', 'g', ''); + objectMetadata.addStrExpression(objectStrExpressionName, 'Obj str', 'd', 'g', ''); + + const behaviorInstance = new gd.BehaviorJsImplementation(); + behaviorInstance.initializeContent = function (behaviorContent) {}; + const behaviorMetadata = extension.addBehavior( + 'TestBehavior', + 'Test behavior', + 'TestBehavior', + 'A test behavior', '', '', - '' + 'TestBehavior', + behaviorInstance, + new gd.BehaviorsSharedData() ); + behaviorMetadata.addExpression(behaviorExpressionName, 'Beh expr', 'd', 'g', ''); + behaviorMetadata.addStrExpression(behaviorStrExpressionName, 'Beh str', 'd', 'g', ''); + + extension.addAction('TestAction', 'Test action', 'Does', 'Does', '', '', ''); + extension.addCondition('TestCondition', 'Test cond', 'Is', 'Is', '', '', ''); + extension.addExpression('TestExpression', 'Test expr', 'd', 'g', ''); + extension.addStrExpression('TestStrExpression', 'Test str', 'd', 'g', ''); + return extension; }; + // Asserts every kind of metadata declared by makeExtension is found (when + // expectFound) or resolves to the "bad" metadata (when not). + const expectAllResolved = (platform, expectFound) => { + const P = gd.MetadataProvider; + const cases = [ + P.isBadObjectMetadata(P.getObjectMetadata(platform, objectType)), + P.isBadBehaviorMetadata(P.getBehaviorMetadata(platform, behaviorType)), + P.isBadInstructionMetadata(P.getActionMetadata(platform, actionType)), + P.isBadInstructionMetadata(P.getConditionMetadata(platform, conditionType)), + P.isBadExpressionMetadata(P.getExpressionMetadata(platform, expressionType)), + P.isBadExpressionMetadata(P.getStrExpressionMetadata(platform, strExpressionType)), + P.isBadExpressionMetadata( + P.getObjectExpressionMetadata(platform, objectType, objectExpressionName) + ), + P.isBadExpressionMetadata( + P.getObjectStrExpressionMetadata(platform, objectType, objectStrExpressionName) + ), + P.isBadExpressionMetadata( + P.getBehaviorExpressionMetadata(platform, behaviorType, behaviorExpressionName) + ), + P.isBadExpressionMetadata( + P.getBehaviorStrExpressionMetadata(platform, behaviorType, behaviorStrExpressionName) + ), + ]; + // When found, none should be "bad"; when not, all should be "bad". + for (const isBad of cases) expect(isBad).toBe(!expectFound); + }; + afterEach(() => { if (gd.JsPlatform.get().isExtensionLoaded(extensionName)) gd.JsPlatform.get().removeExtension(extensionName); }); - it('does not find metadata before the extension is added', () => { - const platform = gd.JsPlatform.get(); - expect( - gd.MetadataProvider.isBadObjectMetadata( - gd.MetadataProvider.getObjectMetadata(platform, objectType) - ) - ).toBe(true); - expect( - gd.MetadataProvider.isBadInstructionMetadata( - gd.MetadataProvider.getActionMetadata(platform, actionType) - ) - ).toBe(true); - }); - - it('finds metadata right after the extension is added (index rebuilt)', () => { + it('resolves every kind of metadata after the extension is added, and none before/after removal', () => { const platform = gd.JsPlatform.get(); - // Build the index once so the test proves it is invalidated, not just - // lazily built for the first time. + // Build the index once first so we test invalidation, not lazy first build. gd.MetadataProvider.getObjectMetadata(platform, 'Sprite'); + expectAllResolved(platform, false); + const extension = makeExtension('First name'); platform.addNewExtension(extension); extension.delete(); - expect( - gd.MetadataProvider.isBadObjectMetadata( - gd.MetadataProvider.getObjectMetadata(platform, objectType) - ) - ).toBe(false); - expect( - gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() - ).toBe('First name'); - expect( - gd.MetadataProvider.isBadInstructionMetadata( - gd.MetadataProvider.getActionMetadata(platform, actionType) - ) - ).toBe(false); + expectAllResolved(platform, true); + + platform.removeExtension(extensionName); + + expectAllResolved(platform, false); }); it('returns updated metadata after the extension is replaced (not stale)', () => { @@ -94,13 +129,12 @@ describe('MetadataProvider index invalidation', () => { platform.addNewExtension(firstExtension); firstExtension.delete(); - // Resolve once so the index caches the first version's metadata. expect( gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() ).toBe('First name'); - // Replacing an extension goes through RemoveExtension + AddExtension, which - // must discard the cached (now dangling) metadata. + // Replacing goes through RemoveExtension + AddExtension and must discard the + // cached (now dangling) metadata. const secondExtension = makeExtension('Second name'); platform.addNewExtension(secondExtension); secondExtension.delete(); @@ -109,32 +143,4 @@ describe('MetadataProvider index invalidation', () => { gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() ).toBe('Second name'); }); - - it('stops finding metadata after the extension is removed', () => { - const platform = gd.JsPlatform.get(); - - const extension = makeExtension('First name'); - platform.addNewExtension(extension); - extension.delete(); - - // Resolve once so the index is populated before removal. - expect( - gd.MetadataProvider.isBadObjectMetadata( - gd.MetadataProvider.getObjectMetadata(platform, objectType) - ) - ).toBe(false); - - platform.removeExtension(extensionName); - - expect( - gd.MetadataProvider.isBadObjectMetadata( - gd.MetadataProvider.getObjectMetadata(platform, objectType) - ) - ).toBe(true); - expect( - gd.MetadataProvider.isBadInstructionMetadata( - gd.MetadataProvider.getActionMetadata(platform, actionType) - ) - ).toBe(true); - }); });