mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-08-25 12:23:58 -04:00
0cd03bbfa1
show in developer changelog
435 lines
16 KiB
JavaScript
435 lines
16 KiB
JavaScript
const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
|
|
|
|
describe('libGD.js object serialization', function () {
|
|
let gd = null;
|
|
beforeAll(async () => {
|
|
gd = await initializeGDevelopJs();
|
|
});
|
|
|
|
describe('gd.SerializerElement', function () {
|
|
it('should support operations on its value', function () {
|
|
var element = new gd.SerializerElement();
|
|
element.setStringValue('aaa');
|
|
expect(element.getStringValue()).toBe('aaa');
|
|
|
|
element.setIntValue(123);
|
|
expect(element.getIntValue()).toBe(123);
|
|
|
|
element.setDoubleValue(123.457);
|
|
expect(element.getDoubleValue()).toBeCloseTo(123.457);
|
|
});
|
|
it('should cast values from a type to another', function () {
|
|
var element = new gd.SerializerElement();
|
|
element.setStringValue('123');
|
|
expect(element.getStringValue()).toBe('123');
|
|
expect(element.getIntValue()).toBe(123);
|
|
expect(element.getDoubleValue()).toBe(123.0);
|
|
|
|
element.setStringValue('true');
|
|
expect(element.getBoolValue()).toBe(true);
|
|
element.setBoolValue(false);
|
|
expect(element.getBoolValue()).toBe(false);
|
|
});
|
|
it('should support operations on its children', function () {
|
|
var element = new gd.SerializerElement();
|
|
|
|
expect(element.hasChild('Missing')).toBe(false);
|
|
var child1 = element.addChild('Child1');
|
|
expect(element.hasChild('Child1')).toBe(true);
|
|
expect(element.getChild('Child1').ptr).toBe(child1.ptr);
|
|
|
|
var child2 = new gd.SerializerElement();
|
|
child2.addChild('subChild').setStringValue('Hello world!');
|
|
element.addChild('Child2');
|
|
element.setChild('Child2', child2);
|
|
|
|
expect(
|
|
element.getChild('Child2').getChild('subChild').getStringValue()
|
|
).toBe('Hello world!');
|
|
});
|
|
});
|
|
|
|
describe('gd.Serializer', function () {
|
|
it('should serialize a Text Object', function () {
|
|
var obj = new gd.TextObject('testObject');
|
|
obj.setText('Text of the object, with 官话 characters');
|
|
|
|
var serializedObject = new gd.SerializerElement();
|
|
obj.serializeTo(serializedObject);
|
|
var jsonObject = gd.Serializer.toJSON(serializedObject);
|
|
serializedObject.delete();
|
|
obj.delete();
|
|
|
|
expect(jsonObject).toBe(
|
|
'{"bold":false,"italic":false,"smoothed":true,"underlined":false,"string":"Text of the object, with 官话 characters","font":"","textAlignment":"left","characterSize":20.0,"color":{"b":0,"g":0,"r":0},"content":{"bold":false,"isOutlineEnabled":false,"isShadowEnabled":false,"italic":false,"outlineColor":"255;255;255","outlineThickness":2.0,"shadowAngle":90.0,"shadowBlurRadius":2.0,"shadowColor":"0;0;0","shadowDistance":4.0,"shadowOpacity":127.0,"smoothed":true,"underlined":false,"text":"Text of the object, with 官话 characters","font":"","textAlignment":"left","verticalTextAlignment":"top","characterSize":20.0,"lineHeight":0.0,"color":"0;0;0"}}'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('gd.Serializer.fromJSON and gd.Serializer.toJSON', function () {
|
|
const checkJsonParseAndStringify = (json) => {
|
|
const element = gd.Serializer.fromJSON(json);
|
|
const outputJson = gd.Serializer.toJSON(element);
|
|
|
|
expect(outputJson).toBe(json);
|
|
};
|
|
|
|
it('should unserialize and reserialize JSON (string)', function () {
|
|
checkJsonParseAndStringify('"a"');
|
|
checkJsonParseAndStringify('"String with 官话 characters"');
|
|
checkJsonParseAndStringify('""');
|
|
});
|
|
it('should unserialize and reserialize JSON (objects)', function () {
|
|
checkJsonParseAndStringify(
|
|
'{"a":{"a1":{"name":"","referenceTo":"/a/a1"}},"b":{"b1":"world"},"c":{"c1":3.0},"things":{"0":{"name":"layout0","referenceTo":"/layouts/layout"},"1":{"name":"layout1","referenceTo":"/layouts/layout"},"2":{"name":"layout2","referenceTo":"/layouts/layout"},"3":{"name":"layout3","referenceTo":"/layouts/layout"},"4":{"name":"layout4","referenceTo":"/layouts/layout"}}}'
|
|
);
|
|
});
|
|
it('should unserialize and reserialize JSON (arrays)', function () {
|
|
checkJsonParseAndStringify('[]');
|
|
checkJsonParseAndStringify('[1]');
|
|
checkJsonParseAndStringify('[1,2]');
|
|
checkJsonParseAndStringify('[{}]');
|
|
checkJsonParseAndStringify('[{"a":1}]');
|
|
checkJsonParseAndStringify('[{"a":1},2]');
|
|
checkJsonParseAndStringify('{"a":[1,2,{"b":3},{"c":[4,5]},6],"7":[]}');
|
|
});
|
|
|
|
it('should parse a large JSON without overflowing the stack', function () {
|
|
// Reproduces #8740: FromJSON copied the whole input into a stack VLA,
|
|
// which overflows the (small, fixed) WASM stack on large projects.
|
|
// ~5 MB of JSON, far larger than the WASM stack.
|
|
const childrenCount = 200000;
|
|
const json =
|
|
'[' +
|
|
new Array(childrenCount).fill('{"name":"obj","value":12345}').join(',') +
|
|
']';
|
|
|
|
const element = gd.Serializer.fromJSON(json);
|
|
// Do NOT delete element, it's a static value.
|
|
|
|
// Round-trips back to the same JSON (also proves the large input was
|
|
// parsed correctly, not just that it didn't crash).
|
|
expect(gd.Serializer.toJSON(element)).toBe(json);
|
|
});
|
|
});
|
|
|
|
// TODO: test failures
|
|
|
|
describe('gd.Serializer.fromJSObject and gd.Serializer.toJSObject', function () {
|
|
const checkJsonParseAndStringify = (json) => {
|
|
const object = JSON.parse(json);
|
|
const element = gd.Serializer.fromJSObject(object);
|
|
const outputObject = gd.Serializer.toJSObject(element);
|
|
|
|
expect(JSON.stringify(outputObject)).toBe(json);
|
|
};
|
|
|
|
it('should unserialize and reserialize JSON (string)', function () {
|
|
checkJsonParseAndStringify('"a"');
|
|
checkJsonParseAndStringify('"String with 官话 characters"');
|
|
checkJsonParseAndStringify('""');
|
|
});
|
|
it('should unserialize and reserialize JSON (objects)', function () {
|
|
checkJsonParseAndStringify(
|
|
'{"a":{"a1":{"name":"","referenceTo":"/a/a1"}},"b":{"b1":"world"},"c":{"c1":3},"things":{"0":{"name":"layout0","referenceTo":"/layouts/layout"},"1":{"name":"layout1","referenceTo":"/layouts/layout"},"2":{"name":"layout2","referenceTo":"/layouts/layout"},"3":{"name":"layout3","referenceTo":"/layouts/layout"},"4":{"name":"layout4","referenceTo":"/layouts/layout"}}}'
|
|
);
|
|
});
|
|
it('should unserialize and reserialize JSON (arrays)', function () {
|
|
checkJsonParseAndStringify('[]');
|
|
checkJsonParseAndStringify('[1]');
|
|
checkJsonParseAndStringify('[1,2]');
|
|
checkJsonParseAndStringify('[{}]');
|
|
checkJsonParseAndStringify('[{"a":1}]');
|
|
checkJsonParseAndStringify('[{"a":1},2]');
|
|
checkJsonParseAndStringify('{"7":[],"a":[1,2,{"b":3},{"c":[4,5]},6]}');
|
|
});
|
|
});
|
|
|
|
describe('gd.Serializer canonical mode', function () {
|
|
afterEach(() => {
|
|
// Always reset, even if a test failed mid-way: leaving the flag on
|
|
// would silently change the format of every subsequent test.
|
|
gd.Serializer.setCanonicalMode(false);
|
|
});
|
|
|
|
it('defaults to off', function () {
|
|
expect(gd.Serializer.isCanonicalMode()).toBe(false);
|
|
});
|
|
|
|
it('toggles on and off', function () {
|
|
gd.Serializer.setCanonicalMode(true);
|
|
expect(gd.Serializer.isCanonicalMode()).toBe(true);
|
|
gd.Serializer.setCanonicalMode(false);
|
|
expect(gd.Serializer.isCanonicalMode()).toBe(false);
|
|
});
|
|
|
|
const buildElement = () => {
|
|
const element = new gd.SerializerElement();
|
|
element.addChild('zeta').setStringValue('z');
|
|
element.addChild('alpha').setStringValue('a');
|
|
element.addChild('mu').setStringValue('m');
|
|
return element;
|
|
};
|
|
|
|
it('keeps insertion key order when off', function () {
|
|
const element = buildElement();
|
|
const json = gd.Serializer.toJSON(element);
|
|
element.delete();
|
|
expect(json).toBe('{"zeta":"z","alpha":"a","mu":"m"}');
|
|
});
|
|
|
|
it('writes JSON keys alphabetically when on', function () {
|
|
gd.Serializer.setCanonicalMode(true);
|
|
const element = buildElement();
|
|
const json = gd.Serializer.toJSON(element);
|
|
element.delete();
|
|
expect(json).toBe('{"alpha":"a","mu":"m","zeta":"z"}');
|
|
});
|
|
|
|
it('round-trips through fromJSON/toJSON in canonical mode', function () {
|
|
gd.Serializer.setCanonicalMode(true);
|
|
const input = '{"a":1,"b":{"x":[1,2,3],"y":"v"},"c":true}';
|
|
const element = gd.Serializer.fromJSON(input);
|
|
const json = gd.Serializer.toJSON(element);
|
|
expect(json).toBe(input);
|
|
});
|
|
|
|
const serializeLayoutEvent = (canonicalMode) => {
|
|
gd.Serializer.setCanonicalMode(canonicalMode);
|
|
|
|
const project = new gd.ProjectHelper.createNewGDJSProject();
|
|
const layout = project.insertNewLayout('Scene', 0);
|
|
const events = layout.getEvents();
|
|
events.insertNewEvent(project, 'BuiltinCommonInstructions::Standard', 0);
|
|
|
|
const element = new gd.SerializerElement();
|
|
events.serializeTo(element);
|
|
const json = gd.Serializer.toJSON(element);
|
|
element.delete();
|
|
project.delete();
|
|
|
|
return JSON.parse(json);
|
|
};
|
|
|
|
it('writes default booleans/empty arrays for an empty StandardEvent when on', function () {
|
|
const eventsArray = serializeLayoutEvent(true);
|
|
expect(Array.isArray(eventsArray)).toBe(true);
|
|
expect(eventsArray.length).toBe(1);
|
|
const event = eventsArray[0];
|
|
|
|
expect(event.disabled).toBe(false);
|
|
expect(event.folded).toBe(false);
|
|
expect(event.events).toEqual([]);
|
|
expect(event.variables).toBeDefined();
|
|
|
|
// Keys must be alphabetical.
|
|
const keys = Object.keys(event);
|
|
const sorted = [...keys].sort();
|
|
expect(keys).toEqual(sorted);
|
|
});
|
|
|
|
it('omits default values for an empty StandardEvent when off', function () {
|
|
const eventsArray = serializeLayoutEvent(false);
|
|
const event = eventsArray[0];
|
|
|
|
expect(event.disabled).toBeUndefined();
|
|
expect(event.folded).toBeUndefined();
|
|
expect(event.events).toBeUndefined();
|
|
expect(event.variables).toBeUndefined();
|
|
});
|
|
|
|
it('produces stable, byte-identical output across two serializations', function () {
|
|
gd.Serializer.setCanonicalMode(true);
|
|
const project = new gd.ProjectHelper.createNewGDJSProject();
|
|
const layout = project.insertNewLayout('Scene', 0);
|
|
layout.getObjects().insertNewObject(project, 'Sprite', 'Object1', 0);
|
|
|
|
const elementA = new gd.SerializerElement();
|
|
project.serializeTo(elementA);
|
|
const jsonA = gd.Serializer.toJSON(elementA);
|
|
elementA.delete();
|
|
|
|
const elementB = new gd.SerializerElement();
|
|
project.serializeTo(elementB);
|
|
const jsonB = gd.Serializer.toJSON(elementB);
|
|
elementB.delete();
|
|
|
|
expect(jsonA).toBe(jsonB);
|
|
|
|
project.delete();
|
|
});
|
|
|
|
it('round-trips through serialize -> deserialize -> serialize', function () {
|
|
gd.Serializer.setCanonicalMode(true);
|
|
const project = new gd.ProjectHelper.createNewGDJSProject();
|
|
project.insertNewLayout('SceneA', 0);
|
|
project.insertNewLayout('SceneB', 1);
|
|
|
|
const elementA = new gd.SerializerElement();
|
|
project.serializeTo(elementA);
|
|
const jsonA = gd.Serializer.toJSON(elementA);
|
|
elementA.delete();
|
|
|
|
const elementB = gd.Serializer.fromJSON(jsonA);
|
|
const project2 = new gd.ProjectHelper.createNewGDJSProject();
|
|
project2.unserializeFrom(elementB);
|
|
|
|
const elementC = new gd.SerializerElement();
|
|
project2.serializeTo(elementC);
|
|
const jsonB = gd.Serializer.toJSON(elementC);
|
|
elementC.delete();
|
|
|
|
expect(jsonB).toBe(jsonA);
|
|
|
|
project.delete();
|
|
project2.delete();
|
|
});
|
|
});
|
|
|
|
describe('gd.BinarySerializer', function () {
|
|
const serializeToBinarySnapshot = (serializerElement) => {
|
|
// Create binary snapshot
|
|
const binaryPtr =
|
|
gd.BinarySerializer.createBinarySnapshot(serializerElement);
|
|
const binarySize = gd.BinarySerializer.getLastBinarySnapshotSize();
|
|
|
|
if (!binaryPtr) {
|
|
throw new Error('Failed to create binary snapshot.');
|
|
}
|
|
|
|
const binaryView = new Uint8Array(
|
|
gd.HEAPU8.buffer,
|
|
binaryPtr,
|
|
binarySize
|
|
);
|
|
// Copy the buffer out of the WASM heap to simulate it was transferred.
|
|
const binaryBuffer = binaryView.slice();
|
|
|
|
gd.BinarySerializer.freeBinarySnapshot(binaryPtr);
|
|
|
|
return binaryBuffer;
|
|
};
|
|
|
|
const serializeJsonToBinarySnapshot = (json) => {
|
|
const element = gd.Serializer.fromJSON(json);
|
|
// Do NOT delete element, it's a static value.
|
|
|
|
return serializeToBinarySnapshot(element);
|
|
};
|
|
|
|
const unserializeBinarySnapshotToJson = (binaryBuffer) => {
|
|
const binaryArray =
|
|
binaryBuffer instanceof Uint8Array
|
|
? binaryBuffer
|
|
: new Uint8Array(binaryBuffer);
|
|
const binarySize = binaryArray.byteLength || binaryArray.length;
|
|
|
|
// Allocate memory in Emscripten heap and copy binary data
|
|
const binaryPtr = gd._malloc(binarySize);
|
|
gd.HEAPU8.set(binaryArray, binaryPtr);
|
|
|
|
const element = gd.BinarySerializer.deserializeBinarySnapshot(
|
|
binaryPtr,
|
|
binarySize
|
|
);
|
|
|
|
// Free the input buffer
|
|
gd._free(binaryPtr);
|
|
|
|
if (element.ptr === 0) {
|
|
throw new Error('Failed to deserialize binary snapshot.');
|
|
}
|
|
|
|
const json = gd.Serializer.toJSON(element);
|
|
element.delete();
|
|
return json;
|
|
};
|
|
|
|
const checkBinaryRoundTrip = (json) => {
|
|
const binaryBuffer = serializeJsonToBinarySnapshot(json);
|
|
expect(binaryBuffer).toBeInstanceOf(Uint8Array);
|
|
expect(binaryBuffer.length).toBeGreaterThan(0);
|
|
|
|
const outputJson = unserializeBinarySnapshotToJson(binaryBuffer);
|
|
expect(outputJson).toBe(json);
|
|
};
|
|
|
|
it('should round-trip simple values', function () {
|
|
checkBinaryRoundTrip('"hello"');
|
|
checkBinaryRoundTrip('"hello"');
|
|
checkBinaryRoundTrip('123');
|
|
checkBinaryRoundTrip('123.456');
|
|
checkBinaryRoundTrip('true');
|
|
checkBinaryRoundTrip('false');
|
|
});
|
|
|
|
it('should round-trip strings with unicode characters', function () {
|
|
checkBinaryRoundTrip('"String with 官话 characters"');
|
|
checkBinaryRoundTrip('"Émojis: 🎮🎲🎯"');
|
|
});
|
|
|
|
it('should round-trip objects', function () {
|
|
checkBinaryRoundTrip('{}');
|
|
checkBinaryRoundTrip('{"a":"b"}');
|
|
checkBinaryRoundTrip('{"a":{"nested":"value"}}');
|
|
checkBinaryRoundTrip(
|
|
'{"a":{"a1":{"name":"","referenceTo":"/a/a1"}},"b":{"b1":"world"},"c":{"c1":3.0}}'
|
|
);
|
|
});
|
|
|
|
it('should round-trip arrays', function () {
|
|
checkBinaryRoundTrip('[]');
|
|
checkBinaryRoundTrip('[1]');
|
|
checkBinaryRoundTrip('[1,2,3]');
|
|
checkBinaryRoundTrip('[{"a":1},{"b":2}]');
|
|
checkBinaryRoundTrip('{"items":[1,2,3],"nested":[{"x":1},{"y":2}]}');
|
|
});
|
|
|
|
it('should round-trip a complex object like a Text Object', function () {
|
|
const obj = new gd.TextObject('testObject');
|
|
obj.setText('Text with 官话 characters');
|
|
|
|
const serializedElement = new gd.SerializerElement();
|
|
obj.serializeTo(serializedElement);
|
|
|
|
const binaryBuffer = serializeToBinarySnapshot(serializedElement);
|
|
const jsonFromBinaryBuffer =
|
|
unserializeBinarySnapshotToJson(binaryBuffer);
|
|
|
|
// Compare JSON output from the original SerializerElement
|
|
// with the JSON obtained from the binary buffer:
|
|
const originalJson = gd.Serializer.toJSON(serializedElement);
|
|
serializedElement.delete();
|
|
obj.delete();
|
|
|
|
expect(jsonFromBinaryBuffer).toBe(originalJson);
|
|
});
|
|
|
|
it('should round-trip a complex object like a Project', function () {
|
|
const project = new gd.ProjectHelper.createNewGDJSProject();
|
|
const layout = project.insertNewLayout('Scene', 0);
|
|
layout.getObjects().insertNewObject(project, 'Sprite', 'Object1', 0);
|
|
layout.getObjects().insertNewObject(project, 'Sprite', 'Object2', 1);
|
|
const instance1 = layout.getInitialInstances().insertNewInitialInstance();
|
|
const instance2 = layout.getInitialInstances().insertNewInitialInstance();
|
|
instance1.setObjectName('Object1');
|
|
instance2.setObjectName('Object2');
|
|
|
|
const serializedElement = new gd.SerializerElement();
|
|
project.serializeTo(serializedElement);
|
|
|
|
const binaryBuffer = serializeToBinarySnapshot(serializedElement);
|
|
const jsonFromBinaryBuffer =
|
|
unserializeBinarySnapshotToJson(binaryBuffer);
|
|
|
|
// Compare JSON output from the original SerializerElement
|
|
// with the JSON obtained from the binary buffer:
|
|
const originalJson = gd.Serializer.toJSON(serializedElement);
|
|
serializedElement.delete();
|
|
project.delete();
|
|
|
|
expect(jsonFromBinaryBuffer).toBe(originalJson);
|
|
});
|
|
});
|
|
});
|