Add tests for the MetadataProvider constant-time index

- Core/tests/PlatformMetadataIndex.cpp (native/catch): every lookup kind against
  DummyPlatform, object-expression fallback to the base ("") type, behavior
  actions indexed alongside free/object ones, bad-metadata for unknown types,
  the declaring extension is returned, and the index rebuilds after an extension
  is removed.
- Expand the jest MetadataProviderIndex test to assert *all* metadata kinds
  resolve after an extension is added and *all* become bad after it is removed
  (invalidation covers the whole index, not just objects/actions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clément Pasteau
2026-06-22 17:32:24 +02:00
parent 60cb6d0059
commit bc662995f4
2 changed files with 193 additions and 73 deletions
+114
View File
@@ -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")));
}
}
+79 -73
View File
@@ -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);
});
});