// 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(` 3x true 9007199254740993 -2 0.5 0.1 y 55555 Thread-7 java.lang.String ab 1 4 constructed k5 `); 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' }); }); });