Warning, /firebird/packages/firebird-ng/workers/root-file/src/lib/root-file.protocol.spec.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * The ROOT file worker's request handling: files open under handles, so one
0003 * client opening a file never replaces the file another client converts, and
0004 * entry selections are checked and clamped to the file before they are
0005 * expanded.
0006 */
0007
0008 import { describe, expect, it } from 'vitest';
0009 import type { DexDocument, PodioModel } from '@dexvis/root2dex';
0010 import {
0011 RootFileSession,
0012 type OpenedConverter,
0013 type RootConvertOptions,
0014 type RootFileRequest,
0015 type RootFileResponse,
0016 } from './root-file.protocol';
0017
0018 /** The options of every conversion the fake converters ran, in order. */
0019 let convertOptions: RootConvertOptions[] = [];
0020
0021 /** A converter that names its file in every converted event and records its options. */
0022 function fakeConverter(name: string, entryCount: number): OpenedConverter {
0023 return {
0024 sourceName: name,
0025 model: 'edm4eic' as PodioModel,
0026 entryCount,
0027 async convert(entries, { collections, trajectoryExcludedCollections }) {
0028 convertOptions.push({ collections, trajectoryExcludedCollections });
0029 return { events: entries.map(entry => ({ id: `${name}#${entry}` })) } as unknown as DexDocument;
0030 },
0031 };
0032 }
0033
0034 /** A request without its id (the helper numbers them). */
0035 type Unsent<T> = T extends unknown ? Omit<T, 'requestId'> : never;
0036
0037 function setup() {
0038 convertOptions = [];
0039 const posted: RootFileResponse[] = [];
0040 const gates = new Map<string, Promise<void>>();
0041 const session = new RootFileSession(
0042 {
0043 listKeys: async () => [{ name: 'events', className: 'TTree' }],
0044 open: async source => {
0045 const name = typeof source === 'string' ? source : source.name;
0046 await gates.get(name);
0047 return fakeConverter(name, 3);
0048 },
0049 collectionGroups: () => ['tracker_hits'],
0050 },
0051 message => posted.push(message),
0052 );
0053 let counter = 0;
0054 const send = async (request: Unsent<RootFileRequest>): Promise<RootFileResponse> => {
0055 const requestId = `r${++counter}`;
0056 await session.handle({ ...request, requestId } as RootFileRequest);
0057 return posted.find(message => message.requestId === requestId)!;
0058 };
0059 const url = (name: string) => ({ kind: 'url' as const, url: name });
0060 const convertedIds = (response: RootFileResponse) =>
0061 response.type === 'converted'
0062 ? (response.dex as unknown as { events: Array<{ id: string }> }).events.map(event => event.id)
0063 : response;
0064 return { session, send, url, gates, convertedIds };
0065 }
0066
0067 describe('RootFileSession', () => {
0068 it('keeps one open file per handle: open(A), open(B), convert(A) converts A', async () => {
0069 const { send, url, convertedIds } = setup();
0070 await send({ type: 'open', handle: 1, source: url('A') });
0071 await send({ type: 'open', handle: 2, source: url('B') });
0072 expect(convertedIds(await send({ type: 'convert', handle: 1, entries: '0' }))).toEqual(['A#0']);
0073 expect(convertedIds(await send({ type: 'convert', handle: 2, entries: '1' }))).toEqual(['B#1']);
0074 });
0075
0076 it('keeps the latest open of a handle when an older one finishes later', async () => {
0077 const { send, url, gates, convertedIds } = setup();
0078 let releaseSlow!: () => void;
0079 gates.set('slow', new Promise<void>(resolve => { releaseSlow = resolve; }));
0080 const slowOpen = send({ type: 'open', handle: 1, source: url('slow') });
0081 await send({ type: 'open', handle: 1, source: url('fast') });
0082 releaseSlow();
0083 await slowOpen;
0084 expect(convertedIds(await send({ type: 'convert', handle: 1, entries: '0' }))).toEqual(['fast#0']);
0085 });
0086
0087 it('closes one handle and leaves the others open', async () => {
0088 const { send, url, convertedIds } = setup();
0089 await send({ type: 'open', handle: 1, source: url('A') });
0090 await send({ type: 'open', handle: 2, source: url('B') });
0091 expect((await send({ type: 'close', handle: 1 })).type).toBe('closed');
0092 expect(await send({ type: 'convert', handle: 1, entries: '0' }))
0093 .toEqual(expect.objectContaining({ type: 'error', error: 'No ROOT file is open' }));
0094 expect(convertedIds(await send({ type: 'convert', handle: 2, entries: '0' }))).toEqual(['B#0']);
0095 });
0096
0097 it('clamps a selection to the file and reports one warning, with the message pyrobird uses', async () => {
0098 const { send, url, convertedIds } = setup();
0099 await send({ type: 'open', handle: 1, source: url('A') });
0100 const converted = await send({ type: 'convert', handle: 1, entries: '0-5' });
0101 expect(convertedIds(converted)).toEqual(['A#0', 'A#1', 'A#2']);
0102 expect(converted).toEqual(expect.objectContaining({
0103 entries: [0, 1, 2],
0104 warnings: ['Event 3-5 is out of range: the file holds 3 events (0..2); converting 0-2'],
0105 }));
0106 });
0107
0108 it('rejects a selection with no entry in the file, or a too large one, with the messages pyrobird uses', async () => {
0109 const { send, url } = setup();
0110 await send({ type: 'open', handle: 1, source: url('A') });
0111 expect(await send({ type: 'convert', handle: 1, entries: '50-60' })).toEqual(expect.objectContaining({
0112 type: 'error',
0113 error: 'Event 50-60 is out of range: the file holds 3 events (0..2)',
0114 }));
0115 expect(await send({ type: 'convert', handle: 1, entries: '0-20000000' })).toEqual(expect.objectContaining({
0116 type: 'error',
0117 error: 'The selection names 20000001 entries; at most 1000 are allowed per request.',
0118 }));
0119 });
0120
0121 it('passes the collection groups and the trajectory exclusions to the converter', async () => {
0122 const { send, url } = setup();
0123 await send({ type: 'open', handle: 1, source: url('A') });
0124 await send({ type: 'convert', handle: 1, entries: '0', collections: ['tracker_hits'], trajectoryExcludedCollections: ['PhotonHits'] });
0125 await send({ type: 'convert', handle: 1, entries: '0', collections: [] });
0126 expect(convertOptions).toEqual([
0127 { collections: ['tracker_hits'], trajectoryExcludedCollections: ['PhotonHits'] },
0128 { collections: undefined, trajectoryExcludedCollections: undefined },
0129 ]);
0130 });
0131
0132 it('probes without a handle', async () => {
0133 const { send, url } = setup();
0134 expect(await send({ type: 'probe', source: url('A') })).toEqual(expect.objectContaining({
0135 type: 'probed',
0136 probe: { name: 'A', entries: [{ name: 'events', className: 'TTree' }] },
0137 }));
0138 });
0139 });