Files
mdg-filius/packages/fls/test/beanCapture.test.ts
T
julian.kiedaischandClaude Opus 5.5 3548652687 feat(fls): read and write a project's own applications as their beans
A project's own applications (classes under projekt/anwendungen, installed
on a host) are no longer left out. The decoder now keeps the Java type of
every primitive — at setters, constructor and call arguments, also through
shared ids like Integer0 — and which properties an object's own getter
made. beanCapture.ts turns a decoded bean back into what the writer writes,
with the node's system software as a mark the writer replaces by the
system software it writes; what the format cannot say is refused, not
guessed (an unresolved value, a Field, a factory-made object, an object a
getter reaches elsewhere in the file). The reader keeps own applications as
captured beans; the corpus no longer warns.

The capture first took a setter with another object's getter result for an
own getter; the round-trip test found it. The Java oracle did not show
whether an application runs on its node's system software, so a mutation
writing the mark instead survived; it now reports onItsSystem (true for all
192 applications of the corpus).

Checked by Java: from the files written again, FILIUS loads Accumulator and
Broadcast installed, switched on and on their node's system software, as
in the original. The round-trip test covers every primitive, null, a
class, an array, shared objects, getter chains, constructed elements, map
reads and static constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:43:16 +00:00

119 lines
7.4 KiB
TypeScript

// A decoded bean written again (beanCapture.ts): decoded, captured, written
// and decoded once more, it holds the same values with the same Java types —
// so XMLDecoder calls the same setters with the same arguments. Java's own
// check is in topologyWriter.test.ts and exportProject.test.ts: the corpus's
// own applications, loaded by FILIUS from the files written again.
import { describe, expect, it } from 'vitest';
import { captureBean, SYSTEM_SOFTWARE_CLASS, withSystemSoftware } from '../src/beanCapture.js';
import { decodeJavaBeans, isJavaObject, type JavaObject, type JavaValue } from '../src/javabeans.js';
import { encodeJavaBeans, object, reference, type BeanObject } from '../src/javaBeansWriter.js';
function doc(body: string): string {
return `<?xml version="1.0" encoding="UTF-8"?>\n<java version="17" class="java.beans.XMLDecoder">\n${body}\n</java>\n`;
}
function bean(value: JavaValue | undefined): JavaObject {
if (!isJavaObject(value)) throw new Error(`not an object: ${JSON.stringify(value)}`);
return value;
}
/** What the decoder keeps of an object, by value: class, arguments, properties and calls with their types. */
function shape(value: JavaValue, seen = new Set<JavaObject>()): unknown {
if (!isJavaObject(value)) return typeof value === 'bigint' ? `${value}n` : value;
if (seen.has(value)) return { shared: value.className };
seen.add(value);
return {
className: value.className,
staticField: value.staticField,
fromGetter: value.fromGetter,
args: value.args.map((arg) => shape(arg, seen)),
argKinds: value.argKinds,
properties: [...value.properties].map(([name, each]) => [name, shape(each, seen), value.propertyKinds.get(name)]),
presetElements: [...value.presetElements].map(([index, each]) => [index, shape(each, seen)]),
presetEntries: [...value.presetEntries].map(([key, each]) => [key, shape(each, seen)]),
calls: value.calls.filter((call) => call.method !== 'get').map((call) => [call.method, call.args.map((arg) => shape(arg, seen)), call.kinds]),
};
}
/** The application of a file: the second value, the first being the system software it refers to. */
const APPLICATION = doc(`<object class="filius.software.system.Betriebssystem" id="Betriebssystem0"/>
<object class="example.OwnApplication">
<short>3</short><char>x</char>
<void property="systemSoftware"><object idref="Betriebssystem0"/></void>
<void property="aktiv"><boolean>true</boolean></void>
<void property="count"><long>9007199254740993</long></void>
<void property="small"><byte>-2</byte></void>
<void property="ratio"><float>0.5</float></void>
<void property="exact"><double>0.1</double></void>
<void property="letter"><char>y</char></void>
<void property="port"><int>55555</int></void>
<void property="name"><string>Thread-7</string></void>
<void property="nothing"><null/></void>
<void property="type"><class>java.lang.String</class></void>
<void property="names"><array class="java.lang.String" length="2"><void index="0"><string>a</string></void><void index="1"><string>b</string></void></array></void>
<void property="first"><object class="java.util.ArrayList" id="ArrayList0"><void method="add"><int>1</int></void></object></void>
<void property="second"><object idref="ArrayList0"/></void>
<void property="alignment"><object class="javax.swing.SwingConstants" field="CENTER"/></void>
<void property="settings"><void property="depth"><int>4</int></void></void>
<void property="list"><void index="0"><void property="label"><string>constructed</string></void></void></void>
<void property="table"><void method="put"><string>k</string><long>5</long></void></void>
</object>`);
describe('captureBean', () => {
it('writes a bean again with the same values and Java types', () => {
const [system, application] = decodeJavaBeans(APPLICATION);
const captured = captureBean(bean(application), system);
if (!('bean' in captured)) throw new Error(captured.failure);
const written = withSystemSoftware(captured.bean, object('filius.software.system.Betriebssystem', [], []));
const [again] = decodeJavaBeans(encodeJavaBeans([written]));
const { properties: _original, ...rest } = shape(application!) as { properties: [string, unknown, unknown][] };
const { properties: _again, ...restAgain } = shape(again!) as { properties: [string, unknown, unknown][] };
expect(restAgain).toEqual(rest);
// The system software is another object now, the one it was given. A
// static constant the decoder answers with its measured value comes back
// as that value, SwingConstants.CENTER as the int it is.
const withoutSystem = (properties: [string, unknown, unknown][]) => properties.filter(([name]) => name !== 'systemSoftware');
const expected = withoutSystem((shape(application!) as { properties: [string, unknown, unknown][] }).properties).map(
([name, value, kind]) => (name === 'alignment' ? [name, value, 'int'] : [name, value, kind]),
);
expect(withoutSystem((shape(again!) as { properties: [string, unknown, unknown][] }).properties)).toEqual(expected);
expect(bean(bean(again).properties.get('systemSoftware')).className).toBe('filius.software.system.Betriebssystem');
// Shared within the bean stays shared.
expect(bean(again).properties.get('first')).toBe(bean(again).properties.get('second'));
});
it("marks the node's system software and puts the given one in its place", () => {
const [system, application] = decodeJavaBeans(APPLICATION);
const captured = captureBean(bean(application), system) as { bean: BeanObject };
const mark = captured.bean.statements!.find((statement) => 'set' in statement && statement.set === 'systemSoftware');
expect(mark).toEqual({ set: 'systemSoftware', value: { kind: 'object', className: SYSTEM_SOFTWARE_CLASS } });
const self = reference('SystemSoftware');
const replaced = withSystemSoftware(captured.bean, self) as BeanObject;
expect(replaced.statements!.find((statement) => 'set' in statement && statement.set === 'systemSoftware')).toEqual({ set: 'systemSoftware', value: self });
// The captured bean itself is left as it was.
expect(captured.bean.statements!).toContainEqual(mark);
});
it('refuses what it cannot write again, and says why', () => {
/** The last value of the file captured, the first being the system software. */
const failure = (body: string) => {
const values = decodeJavaBeans(doc(`<object class="filius.software.system.Betriebssystem" id="Betriebssystem0"/>${body}`));
return captureBean(bean(values.at(-1)), values[0]);
};
expect(failure(`<object class="example.A"><void property="x"><object idref="Missing0"/></void></object>`)).toEqual({
failure: 'a value is unresolved (idref Missing0 is not defined)',
});
expect(
failure(`<object class="example.A"><void property="x"><object class="example.Factory" method="create"/></void></object>`),
).toEqual({ failure: 'example.Factory.create makes an object this writer cannot' });
// A folder of the file system, reached elsewhere by a getter: a copy would not be that folder.
expect(
failure(`<object class="example.B"><void id="DefaultMutableTreeNode0" property="root"><void property="x"><int>1</int></void></void></object>
<object class="example.A"><void property="folder"><object idref="DefaultMutableTreeNode0"/></void></object>`),
).toEqual({ failure: 'it refers to DefaultMutableTreeNode, which a getter reaches elsewhere in the file' });
});
});