// 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 `\n\n${body}\n\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()): 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(`
`);
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(`${body}`));
return captureBean(bean(values.at(-1)), values[0]);
};
expect(failure(``)).toEqual({
failure: 'a value is unresolved (idref Missing0 is not defined)',
});
expect(
failure(``),
).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(`1`),
).toEqual({ failure: 'it refers to DefaultMutableTreeNode, which a getter reaches elsewhere in the file' });
});
});