Back to home page

EIC code displayed by LXR

 
 

    


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 });