From 66f03cc28d9998a45047219d07e5cfbc60791b57 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Tue, 4 Aug 2026 22:11:23 +0800 Subject: [PATCH 1/4] Update the animator contour match test for matched image tiles --- src/test/ANIMATOR_CONTOUR_MATCH.test.ts | 450 ++++++++++++++++-------- 1 file changed, 308 insertions(+), 142 deletions(-) diff --git a/src/test/ANIMATOR_CONTOUR_MATCH.test.ts b/src/test/ANIMATOR_CONTOUR_MATCH.test.ts index 8141964..36f5ea7 100644 --- a/src/test/ANIMATOR_CONTOUR_MATCH.test.ts +++ b/src/test/ANIMATOR_CONTOUR_MATCH.test.ts @@ -1,31 +1,38 @@ import { CARTA } from 'carta-protobuf'; -import { checkConnection, Stream } from './MyClient'; import * as Long from 'long'; +import { Subscription } from 'rxjs'; +import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; import config from './config.json'; -import { take } from 'rxjs/operators'; let connectTimeout = config.timeout.connection; let testServerUrl: string = config.serverURL0; let testSubdirectory: string = config.path.QA; let openFileTimeout: number = config.timeout.openFile; +let readFileTimeout: number = config.timeout.readFile; +let playAnimatorTimeout: number = config.timeout.playAnimator; +let changeChannelTimeout: number = config.timeout.changeChannel; +let messageReturnTimeout: number = config.timeout.messageEvent; interface AssertItem { - registerViewer: CARTA.IRegisterViewer; filelist: CARTA.IFileListRequest; fileOpen: CARTA.IOpenFile[]; - addTilesReq: CARTA.IAddRequiredTiles; + initTilesReq: CARTA.IAddRequiredTiles[]; setContour: CARTA.ISetContourParameters[]; startAnimation: CARTA.IStartAnimation; + matchedTilesReq: CARTA.IAddRequiredTiles; + hiddenTilesReq: CARTA.IAddRequiredTiles; stopAnimation: CARTA.IStopAnimation; setImageChannel: CARTA.ISetImageChannels[]; + milestone: { + requestMatchedTiles: number; + hideMatchedImage: number; + restoreMatchedImage: number; + stop: number; + }; } let assertItem: AssertItem = { - registerViewer: { - sessionId: 0, - clientFeatureFlags: 5, - }, filelist: { directory: testSubdirectory }, fileOpen: [ { @@ -43,12 +50,20 @@ let assertItem: AssertItem = { renderMode: CARTA.RenderMode.RASTER, }, ], - addTilesReq: { - fileId: 0, - compressionQuality: 11, - compressionType: CARTA.CompressionType.ZFP, - tiles: [0], - }, + initTilesReq: [ + { + fileId: 0, + compressionQuality: 11, + compressionType: CARTA.CompressionType.ZFP, + tiles: [0], + }, + { + fileId: 1, + compressionQuality: 11, + compressionType: CARTA.CompressionType.ZFP, + tiles: [0], + }, + ], setContour: [ { fileId: 0, @@ -106,9 +121,25 @@ let assertItem: AssertItem = { }, }, }, + // Sent while the animation is running. This is the only way the matched image can acquire + // animation view settings: outside an animation ADD_REQUIRED_TILES returns tile data instead. + matchedTilesReq: { + fileId: 1, + tiles: [0], + compressionType: CARTA.CompressionType.ZFP, + compressionQuality: 9, + }, + // What the frontend sends when a matched image scrolls out of view during animation: + // AppStore's view autorun pushes a view update with an empty tile list for it. + hiddenTilesReq: { + fileId: 1, + tiles: [], + compressionType: CARTA.CompressionType.ZFP, + compressionQuality: 9, + }, stopAnimation: { fileId: 0, - endFrame: { channel: 10, stokes: 0 }, + endFrame: { channel: 21, stokes: 0 }, }, setImageChannel: [ { @@ -127,17 +158,106 @@ let assertItem: AssertItem = { channel: 0, stokes: 0, requiredTiles: { - fileId: 0, + fileId: 1, tiles: [0], compressionType: CARTA.CompressionType.ZFP, compressionQuality: 11, }, }, ], + // The active file's channel at which each client message is sent. + milestone: { + requestMatchedTiles: 3, + hideMatchedImage: 9, + restoreMatchedImage: 15, + stop: 21, + }, }; +/** + * The backend runs at most one animation frame ahead of the last flow-control ack, and a client + * message queued mid-frame lands a frame or two later. Each phase is therefore asserted over the + * last three channels before the next milestone, which leaves the three channels right after a + * milestone as an unasserted settling band. + */ +const WINDOW_CHANNELS = 3; + +function assertedChannels(milestone: number): number[] { + let channels: number[] = []; + for (let channel = milestone - WINDOW_CHANNELS + 1; channel <= milestone; channel++) { + channels.push(channel); + } + return channels; +} + +interface AnimationRecord { + sync: CARTA.RasterTileSync[]; + tile: CARTA.RasterTileData[]; + contour: CARTA.ContourImageData[]; + histogram: CARTA.RegionHistogramData[]; +} + +/** + * Subscribe to every animation stream for the whole playback rather than per frame. + * + * The RxJS subjects do not buffer, so a per-frame subscribe/await loop can drop messages of the + * frame the backend is already working on. Subscribing once up front also removes any assumption + * about the order in which the active and the matched image are served within a frame. + * + * Flow control is acknowledged from the active file's tile, which is what paces the animation. + */ +function collectAnimation(activeFileId: number) { + const msgController = MessageController.Instance; + const record: AnimationRecord = { sync: [], tile: [], contour: [], histogram: [] }; + let lastActiveChannel = -1; + let waiters: { channel: number; resolve: () => void }[] = []; + + const settle = () => { + waiters = waiters.filter((waiter) => { + if (lastActiveChannel >= waiter.channel) { + waiter.resolve(); + return false; + } + return true; + }); + }; + + const subscriptions: Subscription[] = [ + msgController.rasterSyncStream.subscribe((data) => record.sync.push(data)), + msgController.rasterTileStream.subscribe((data) => { + record.tile.push(data); + if (data.fileId === activeFileId) { + lastActiveChannel = data.channel; + msgController.sendAnimationFlowControl({ + fileId: activeFileId, + animationId: 0, + receivedFrame: { channel: data.channel, stokes: data.stokes }, + timestamp: Long.fromNumber(Date.now()), + }); + settle(); + } + }), + msgController.contourStream.subscribe((data) => record.contour.push(data)), + msgController.histogramStream.subscribe((data) => record.histogram.push(data)), + ]; + + return { + record, + // Resolves once the active file has delivered the tile of the given channel. + reached: (channel: number) => + new Promise((resolve) => { + if (lastActiveChannel >= channel) { + resolve(); + } else { + waiters.push({ channel, resolve }); + } + }), + stop: () => subscriptions.forEach((subscription) => subscription.unsubscribe()), + }; +} + let basepath: string; -describe('ANIMATOR_CONTOUR: Testing animation playback with contour lines', () => { +describe('ANIMATOR_CONTOUR_MATCH: Testing animation playback of a spectrally matched image', () => { const msgController = MessageController.Instance; describe(`Register a session`, () => { beforeAll(async () => { @@ -172,149 +292,195 @@ describe('ANIMATOR_CONTOUR: Testing animation playback with contour lines', () = }); describe(`Preparation`, () => { - test(`Contour set`, async () => { - msgController.addRequiredTiles(assertItem.addTilesReq); - let RasterTileDataResponse = await Stream( - CARTA.RasterTileData, - assertItem.addTilesReq.tiles.length + 2 - ); - - msgController.setContourParameters(assertItem.setContour[0]); - let ContourImageDataResponse1 = await Stream( - CARTA.ContourImageData, - assertItem.setContour[0].levels.length - ); + test( + `Render both images and set matched contours`, + async () => { + for (let i = 0; i < assertItem.initTilesReq.length; i++) { + let rasterResponse = Stream(CARTA.RasterTileData, assertItem.initTilesReq[i].tiles!.length + 2); + msgController.addRequiredTiles(assertItem.initTilesReq[i]); + await rasterResponse; + } - msgController.setContourParameters(assertItem.setContour[1]); - let ContourImageDataResponse2 = await Stream( - CARTA.ContourImageData, - assertItem.setContour[1].levels.length - ); - }); + for (let i = 0; i < assertItem.setContour.length; i++) { + let contourResponse = Stream(CARTA.ContourImageData, assertItem.setContour[i].levels!.length); + msgController.setContourParameters(assertItem.setContour[i]); + await contourResponse; + } + }, + readFileTimeout * 2 + ); }); - describe(`Play some channels forwardly`, () => { - let regionHistogramData: CARTA.RegionHistogramData[] = []; - let sequence: number[] = []; - let contourImageData: CARTA.ContourImageData[] = []; - let HistogramSequence: number[] = []; - let ContourSequence: number[] = []; - test(`Assert ContourImageData.channel = RasterTileData.channel`, async () => { - let StartAnimationResponse = await msgController.startAnimation(assertItem.startAnimation); - expect(StartAnimationResponse.success).toEqual(true); - - for (let i = 0; i < assertItem.stopAnimation.endFrame.channel; i++) { - msgController.addRequiredTiles(assertItem.addTilesReq); - let resRegionHistogramData = msgController.histogramStream.pipe(take(2)).subscribe({ - next: (data) => { - regionHistogramData.push(data); - HistogramSequence.push(data.channel); - }, - }); - let rasterTileDataResponse = await Stream(CARTA.RasterTileData, 3); - let resContourImageData = msgController.contourStream.pipe(take(4)).subscribe({ - next: (data) => { - contourImageData.push(data); - ContourSequence.push(data.channel); - }, - }); - let currentChannel = rasterTileDataResponse[0].channel; - sequence.push(currentChannel); - msgController.sendAnimationFlowControl({ - fileId: 0, - animationId: 0, - receivedFrame: { - channel: currentChannel, - stokes: 0, - }, - timestamp: Long.fromNumber(Date.now()), - }); - } + describe(`Play the animation of the reference image`, () => { + let record: AnimationRecord; - // // Pick up the streaming messages - // // Channel 11 & 12: RasterTileData + RasterTileSync(start & end) + RegionHistogramData - // let RegionHistogramDataChannel11: CARTA.RegionHistogramData[] = []; - // msgController.histogramStream.pipe(take(1)).subscribe(data => { - // RegionHistogramDataChannel11.push(data) - // }); - // let ContourImageDataChannel11 = await Stream(CARTA.ContourImageData,4) - // console.log(ContourImageDataChannel11); - // let RasterTileDataChannel11 = await Stream(CARTA.RasterTileData,3); - // console.log(RasterTileDataChannel11); - - // let RegionHistogramDataChannel12: CARTA.RegionHistogramData[] = []; - // msgController.histogramStream.pipe(take(1)).subscribe(data => { - // RegionHistogramDataChannel12.push(data) - // }); - // let ContourImageDataChannel12 = await Stream(CARTA.ContourImageData,4) - // console.log(ContourImageDataChannel12); - // let RasterTileDataChannel12 = await Stream(CARTA.RasterTileData,3); - // console.log(RasterTileDataChannel12) - }); + test( + `Play up to channel ${assertItem.milestone.stop}, hiding and restoring the matched image`, + async () => { + const collector = collectAnimation(assertItem.startAnimation.fileId!); + record = collector.record; - test(`Assert the last channel = StopAnimation.endFrame`, async () => { - msgController.stopAnimation(assertItem.stopAnimation); - msgController.setChannels(assertItem.setImageChannel[0]); - let lastRegionHistogramData1: CARTA.RegionHistogramData[] = []; - msgController.histogramStream.pipe(take(1)).subscribe({ - next: (data) => { - lastRegionHistogramData1.push(data); - }, + let StartAnimationResponse = await msgController.startAnimation(assertItem.startAnimation); + expect(StartAnimationResponse.success).toEqual(true); + + // The matched image has no animation view settings yet, so it is not tiled. + await collector.reached(assertItem.milestone.requestMatchedTiles); + msgController.addRequiredTiles(assertItem.matchedTilesReq); + + // The matched image goes out of view: an empty tile list stops its tiles. + await collector.reached(assertItem.milestone.hideMatchedImage); + msgController.addRequiredTiles(assertItem.hiddenTilesReq); + + // The matched image comes back into view. + await collector.reached(assertItem.milestone.restoreMatchedImage); + msgController.addRequiredTiles(assertItem.matchedTilesReq); + + await collector.reached(assertItem.milestone.stop); + // The matched image is served after the reference image within a frame, so let + // the rest of the last asserted frame arrive before unsubscribing. + await new Promise((resolve) => setTimeout(resolve, messageReturnTimeout)); + msgController.stopAnimation(assertItem.stopAnimation); + collector.stop(); + }, + playAnimatorTimeout + ); + + test(`START_ANIMATION_ACK.success = True and the reference channels are in sequence`, () => { + let channels = record.tile + .filter((data) => data.fileId === assertItem.startAnimation.fileId) + .map((data) => data.channel); + channels.map((channel, index) => { + expect(channel).toEqual(assertItem.startAnimation.startFrame!.channel! + index); }); - let lastContourImageData1: CARTA.ContourImageData[] = []; - msgController.contourStream.pipe(take(2)).subscribe({ - next: (data) => { - lastContourImageData1.push(data); - }, + expect(channels.length).toBeGreaterThanOrEqual(assertItem.milestone.stop); + }); + + describe(`Before ADD_REQUIRED_TILES of the matched image`, () => { + assertedChannels(assertItem.milestone.requestMatchedTiles).map((channel) => { + test(`Channel ${channel}: only the reference image is tiled`, () => { + expect( + record.tile.filter((data) => data.fileId === 0 && data.channel === channel).length + ).toEqual(1); + expect( + record.tile.filter((data) => data.fileId === 1 && data.channel === channel).length + ).toEqual(0); + expect( + record.sync.filter((data) => data.fileId === 1 && data.channel === channel).length + ).toEqual(0); + }); }); + }); - let lastRasterTileData1 = await Stream(CARTA.RasterTileData, 3); + describe(`After ADD_REQUIRED_TILES of the matched image`, () => { + assertedChannels(assertItem.milestone.hideMatchedImage).map((channel) => { + test(`Channel ${channel}: both images are tiled`, () => { + assertItem.fileOpen.map((file) => { + let tiles = record.tile.filter( + (data) => data.fileId === file.fileId && data.channel === channel + ); + expect(tiles.length).toEqual(1); + expect(tiles[0].stokes).toEqual(0); - msgController.setChannels(assertItem.setImageChannel[1]); - let lastRegionHistogramData2: CARTA.RegionHistogramData[] = []; - msgController.histogramStream.pipe(take(1)).subscribe({ - next: (data) => { - lastRegionHistogramData2.push(data); - }, + let syncs = record.sync.filter( + (data) => data.fileId === file.fileId && data.channel === channel + ); + expect(syncs.length).toEqual(2); + expect(syncs.filter((data) => data.endSync).length).toEqual(1); + expect(syncs.filter((data) => data.tileCount === 1).length).toEqual(2); + }); + }); }); - let lastContourImageData2: CARTA.ContourImageData[] = []; - msgController.contourStream.pipe(take(2)).subscribe({ - next: (data) => { - lastContourImageData2.push(data); - }, + }); + + describe(`After ADD_REQUIRED_TILES of the matched image with an empty tile list`, () => { + assertedChannels(assertItem.milestone.restoreMatchedImage).map((channel) => { + test(`Channel ${channel}: only the reference image is tiled`, () => { + expect( + record.tile.filter((data) => data.fileId === 0 && data.channel === channel).length + ).toEqual(1); + expect( + record.tile.filter((data) => data.fileId === 1 && data.channel === channel).length + ).toEqual(0); + expect( + record.sync.filter((data) => data.fileId === 1 && data.channel === channel).length + ).toEqual(0); + }); }); + }); - let lastRasterTileData2 = await Stream(CARTA.RasterTileData, 3); + describe(`After the matched image is restored`, () => { + assertedChannels(assertItem.milestone.stop).map((channel) => { + test(`Channel ${channel}: both images are tiled`, () => { + assertItem.fileOpen.map((file) => { + expect( + record.tile.filter((data) => data.fileId === file.fileId && data.channel === channel) + .length + ).toEqual(1); + expect( + record.sync.filter((data) => data.fileId === file.fileId && data.channel === channel) + .length + ).toEqual(2); + }); + }); + }); }); - test(`Received image channels should be in sequence`, async () => { - sequence.map((id, index) => { - let channelId = - index + - assertItem.startAnimation.startFrame.channel + - assertItem.startAnimation.deltaFrame.channel; - expect(id).toEqual(channelId - 1); + describe(`Contours and histograms of the matched image`, () => { + // The matched image keeps stepping through its own channels even while it is not + // tiled, so its contours and histograms arrive in every phase. + let allChannels = [ + ...assertedChannels(assertItem.milestone.requestMatchedTiles), + ...assertedChannels(assertItem.milestone.hideMatchedImage), + ...assertedChannels(assertItem.milestone.restoreMatchedImage), + ...assertedChannels(assertItem.milestone.stop), + ]; + + allChannels.map((channel) => { + test(`Channel ${channel}: both images return contours and a histogram`, () => { + assertItem.setContour.map((contour) => { + let contours = record.contour.filter( + (data) => + data.fileId === contour.fileId && data.channel === channel && data.progress === 1 + ); + expect(contours.length).toEqual(contour.levels!.length); + contours.map((data) => { + expect(data.referenceFileId).toEqual(contour.referenceFileId); + }); + + expect( + record.histogram.filter( + (data) => data.fileId === contour.fileId && data.channel === channel + ).length + ).toEqual(1); + }); + }); }); }); + }); - test(`Assert a series of ContourImageData`, async () => { - for (let i = 2; i <= assertItem.stopAnimation.endFrame.channel; i++) { - let testSet = contourImageData.filter((data) => data.progress == 1 && data.channel == i); - expect(testSet.length).toEqual(assertItem.setContour[0].levels.length * assertItem.fileOpen.length); - expect(testSet.filter((data) => data.fileId == 0).length).toEqual( - assertItem.setContour[0].levels.length - ); - expect(testSet.filter((data) => data.fileId == 1).length).toEqual( - assertItem.setContour[1].levels.length - ); - } - expect(contourImageData.length).toEqual( - assertItem.stopAnimation.endFrame.channel * - assertItem.setContour[0].levels.length * - assertItem.fileOpen.length + describe(`Set the channel of each image after STOP_ANIMATION`, () => { + assertItem.setImageChannel.map((setImageChannel) => { + let rasterTileData: CARTA.RasterTileData[]; + + test( + `File ${setImageChannel.fileId}: SET_IMAGE_CHANNELS returns RASTER_TILE_DATA`, + async () => { + let rasterResponse = Stream( + CARTA.RasterTileData, + setImageChannel.requiredTiles!.tiles!.length + 2 + ); + msgController.setChannels(setImageChannel); + rasterTileData = (await rasterResponse).filter( + (data: any) => data instanceof CARTA.RasterTileData + ); + }, + changeChannelTimeout ); - contourImageData.map((data) => { - expect(data.referenceFileId).toEqual(1); + + test(`File ${setImageChannel.fileId}: RASTER_TILE_DATA.channel = ${setImageChannel.channel}`, () => { + expect(rasterTileData.length).toEqual(1); + expect(rasterTileData[0].fileId).toEqual(setImageChannel.fileId); + expect(rasterTileData[0].channel).toEqual(setImageChannel.channel); }); }); }); From 2d3075f5234953b523758a9fd1aa63ecd21fc7bc Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Tue, 4 Aug 2026 22:11:46 +0800 Subject: [PATCH 2/4] Document the matched image tile checks in the animator tests --- docs/source/animator.rst | 105 +++++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 11 deletions(-) diff --git a/docs/source/animator.rst b/docs/source/animator.rst index 5639fe4..f7e2107 100644 --- a/docs/source/animator.rst +++ b/docs/source/animator.rst @@ -418,6 +418,24 @@ ANIMATOR_CONTOUR_MATCH See the `source code `__. +This test verifies that a spectrally matched image is animated alongside the reference image: it +steps through its own matched channels, returns contours and histograms at every step, and is tiled +only while the frontend is actually rendering it. + +The tiles of a matched image are controlled entirely by **ADD_REQUIRED_TILES** sent *during* the +animation. Outside an animation that message returns tile data immediately and does not become an +animation setting, so a matched image opened before **START_ANIMATION** is not tiled until the +frontend sends a tile request for it while the animation runs. Sending one with an empty tile list — +which is what the frontend does when a matched image is no longer visible — stops its tiles again +without stopping its channel updates. + +.. note:: + + The backend runs at most one animation frame ahead of the last flow-control acknowledgement, and + a message sent mid-frame takes effect a frame or two later. Each phase below is therefore checked + over the last three channels before the next client message, leaving the three channels after each + message unchecked. + 1. Frontend sends: **OPEN_FILE** (``OpenFile``) for two files File 1: @@ -442,11 +460,11 @@ See the `source code Date: Wed, 5 Aug 2026 09:54:15 +0800 Subject: [PATCH 3/4] Declare the animation collector helpers next to their state --- src/test/ANIMATOR_CONTOUR_MATCH.test.ts | 27 +++++++++++++------------ 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/test/ANIMATOR_CONTOUR_MATCH.test.ts b/src/test/ANIMATOR_CONTOUR_MATCH.test.ts index 36f5ea7..a50e38b 100644 --- a/src/test/ANIMATOR_CONTOUR_MATCH.test.ts +++ b/src/test/ANIMATOR_CONTOUR_MATCH.test.ts @@ -212,6 +212,16 @@ function collectAnimation(activeFileId: number) { let lastActiveChannel = -1; let waiters: { channel: number; resolve: () => void }[] = []; + // Resolves once the active file has delivered the tile of the given channel. + const reached = (channel: number) => + new Promise((resolve) => { + if (lastActiveChannel >= channel) { + resolve(); + } else { + waiters.push({ channel, resolve }); + } + }); + const settle = () => { waiters = waiters.filter((waiter) => { if (lastActiveChannel >= waiter.channel) { @@ -241,19 +251,10 @@ function collectAnimation(activeFileId: number) { msgController.histogramStream.subscribe((data) => record.histogram.push(data)), ]; - return { - record, - // Resolves once the active file has delivered the tile of the given channel. - reached: (channel: number) => - new Promise((resolve) => { - if (lastActiveChannel >= channel) { - resolve(); - } else { - waiters.push({ channel, resolve }); - } - }), - stop: () => subscriptions.forEach((subscription) => subscription.unsubscribe()), - }; + // Ends the collection once the playback is over. + const stop = () => subscriptions.forEach((subscription) => subscription.unsubscribe()); + + return { record, reached, stop }; } let basepath: string; From 2e8a4c9b9a1f981227d6375c80801c68b7e4d5df Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 5 Aug 2026 11:22:16 +0800 Subject: [PATCH 4/4] Exclude the milestone channel from the matched image tile checks Within one animation frame the backend serves the reference image and then each matched image, and the test sends ADD_REQUIRED_TILES as soon as the reference image tile arrives, so the message can still take effect on the matched image of that same frame. The milestone channel was therefore ambiguous, and the macOS CI runner won the race the Linux runners lose. Check the three channels before each milestone instead of ending on it, and move the milestones to 4, 10, 16 and 22 to keep the checked channels at 1 to 3, 7 to 9, 13 to 15 and 19 to 21. --- docs/source/animator.rst | 18 ++++++++++++------ src/test/ANIMATOR_CONTOUR_MATCH.test.ts | 25 +++++++++++++++---------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/docs/source/animator.rst b/docs/source/animator.rst index f7e2107..cc98622 100644 --- a/docs/source/animator.rst +++ b/docs/source/animator.rst @@ -433,8 +433,13 @@ without stopping its channel updates. The backend runs at most one animation frame ahead of the last flow-control acknowledgement, and a message sent mid-frame takes effect a frame or two later. Each phase below is therefore checked - over the last three channels before the next client message, leaving the three channels after each - message unchecked. + over the three channels before the next client message, leaving the channels around each message + unchecked. + + The channel on which a message is sent is itself unchecked. Within one frame the backend serves + the reference image and then each matched image, and the frontend sends the message as soon as the + reference image tile arrives, so the message can still take effect on the matched image of that + same frame. The channel before it is the last one whose matched image has already been served. 1. Frontend sends: **OPEN_FILE** (``OpenFile``) for two files @@ -534,7 +539,7 @@ without stopping its channel updates. **The matched image becomes visible** 7. Frontend sends: **ADD_REQUIRED_TILES** (``AddRequiredTiles``) for the matched image, while the - animation is running + animation is running, on channel 4 .. code-block:: protobuf @@ -552,7 +557,7 @@ without stopping its channel updates. **The matched image is no longer visible** 8. Frontend sends: **ADD_REQUIRED_TILES** (``AddRequiredTiles``) for the matched image with an empty - tile list + tile list, on channel 10 .. code-block:: protobuf @@ -569,7 +574,8 @@ without stopping its channel updates. **The matched image is visible again** -9. Frontend sends: **ADD_REQUIRED_TILES** (``AddRequiredTiles``) for the matched image, as in step 7 +9. Frontend sends: **ADD_REQUIRED_TILES** (``AddRequiredTiles``) for the matched image, as in step 7, + on channel 16 :red-text:`Check 5:` channels 19 to 21 should satisfy, for both file_id = 0 and file_id = 1: @@ -591,7 +597,7 @@ without stopping its channel updates. .. code-block:: protobuf file_id = 0 - end_frame = {channel: 21, stokes: 0} + end_frame = {channel: 22, stokes: 0} 11. Frontend sends: **SET_IMAGE_CHANNELS** (``SetImageChannels``) for each file at channel 0, requesting one tile diff --git a/src/test/ANIMATOR_CONTOUR_MATCH.test.ts b/src/test/ANIMATOR_CONTOUR_MATCH.test.ts index a50e38b..0c806f7 100644 --- a/src/test/ANIMATOR_CONTOUR_MATCH.test.ts +++ b/src/test/ANIMATOR_CONTOUR_MATCH.test.ts @@ -139,7 +139,7 @@ let assertItem: AssertItem = { }, stopAnimation: { fileId: 0, - endFrame: { channel: 21, stokes: 0 }, + endFrame: { channel: 22, stokes: 0 }, }, setImageChannel: [ { @@ -167,24 +167,29 @@ let assertItem: AssertItem = { ], // The active file's channel at which each client message is sent. milestone: { - requestMatchedTiles: 3, - hideMatchedImage: 9, - restoreMatchedImage: 15, - stop: 21, + requestMatchedTiles: 4, + hideMatchedImage: 10, + restoreMatchedImage: 16, + stop: 22, }, }; /** * The backend runs at most one animation frame ahead of the last flow-control ack, and a client * message queued mid-frame lands a frame or two later. Each phase is therefore asserted over the - * last three channels before the next milestone, which leaves the three channels right after a - * milestone as an unasserted settling band. + * three channels before its milestone, which leaves the channels around a milestone as an + * unasserted settling band. + * + * The milestone channel itself is excluded: within a frame the backend serves the reference image + * and then each matched image, and the milestone message is sent on the reference image's tile, so + * it can still land before the matched image of that same frame is served. Channel milestone - 1 is + * the last one whose matched image is already out. */ const WINDOW_CHANNELS = 3; function assertedChannels(milestone: number): number[] { let channels: number[] = []; - for (let channel = milestone - WINDOW_CHANNELS + 1; channel <= milestone; channel++) { + for (let channel = milestone - WINDOW_CHANNELS; channel <= milestone - 1; channel++) { channels.push(channel); } return channels; @@ -336,9 +341,9 @@ describe('ANIMATOR_CONTOUR_MATCH: Testing animation playback of a spectrally mat await collector.reached(assertItem.milestone.restoreMatchedImage); msgController.addRequiredTiles(assertItem.matchedTilesReq); + // The reference image's tile of the stop channel implies every message of the + // last asserted frame is already out, but leave a margin before unsubscribing. await collector.reached(assertItem.milestone.stop); - // The matched image is served after the reference image within a frame, so let - // the rest of the last asserted frame arrive before unsubscribing. await new Promise((resolve) => setTimeout(resolve, messageReturnTimeout)); msgController.stopAnimation(assertItem.stopAnimation); collector.stop();