From 09f7cc6831752bc53995ec51fb6582f2443655e3 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 19 Aug 2026 10:50:11 +0800 Subject: [PATCH 1/7] Assert every refusal in the Stokes concatenation error test --- ICD_test_stages/file_browser.tests | 1 + docs/source/concat_stokes.rst | 154 +++++++++++- src/test/CONCAT_ERROR_MESSAGE.test.ts | 339 ++++++++++++++++++++------ 3 files changed, 419 insertions(+), 75 deletions(-) diff --git a/ICD_test_stages/file_browser.tests b/ICD_test_stages/file_browser.tests index be923345..affe0efa 100644 --- a/ICD_test_stages/file_browser.tests +++ b/ICD_test_stages/file_browser.tests @@ -3,3 +3,4 @@ src/test/GET_FILELIST_ROOTPATH_CONCURRENT.test.ts src/test/FILEINFO_FITS_MULTIHDU.test.ts src/test/FILEINFO_EXCEPTIONS.test.ts src/test/OPEN_SWAPPED_IMAGES.test.ts +src/test/CONCAT_ERROR_MESSAGE.test.ts diff --git a/docs/source/concat_stokes.rst b/docs/source/concat_stokes.rst index 87baf49e..0a38bb29 100644 --- a/docs/source/concat_stokes.rst +++ b/docs/source/concat_stokes.rst @@ -194,7 +194,26 @@ CONCAT_ERROR_MESSAGE See the `source code `__. -This test verifies that attempting to concatenate incompatible Stokes images returns appropriate error messages. +This test verifies that a **CONCAT_STOKES_FILES** request which cannot be satisfied is refused with an +explanatory message, that the refusal costs nothing else, and that the session is unharmed by it. + +Every refusal travels back the same way: the backend answers with a **CONCAT_STOKES_FILES_ACK** +carrying ``success = False`` and the reason in ``message``, and it opens no image. Because that ack is +the whole of the answer, each case is followed by two further checks. The first watches the connection +for 500 ms to confirm that no **REGION_HISTOGRAM_DATA** or raster data was streamed for an image which +was never opened. The second sends a **SET_SPATIAL_REQUIREMENTS** naming the requested ``file_id``: a +file id which is genuinely free draws ``ERROR_DATA`` reading "File id 0 not found", so this is what +shows that the refused request left no half-opened image behind. The frontend leaves its own file +counter unchanged when the request is rejected, so a file id which was quietly consumed would put the +two sides out of step. + +The six refusals below cover every rejection ``StokesFilesConnector`` can raise for the images in +``set_QA``. A seventh case then concatenates a valid pair to show the session still works. + +The test opens with **FILE_LIST_REQUEST**, first against ``$BASE`` to resolve the base path the +Stokes file directories are then prefixed with, and again against ``set_QA`` to verify that every +image the cases name is on disk — apart from the one Case 6 deliberately invents. Without that, any +of the refusals below could be a missing file wearing a different case title. **Case 1: Inconsistent image shapes (Q + axis-degeneracy U)** @@ -209,9 +228,17 @@ This test verifies that attempting to concatenate incompatible Stokes images ret {file: "IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits", polarizationType: 3} ] -:red-text:`Check 1:` the error response should contain: +:red-text:`Check 1:` the CONCAT_STOKES_FILES_ACK should satisfy: - - Error message containing "are not consistent!" + - success = False + - message contains "Image shapes or axes are not consistent!" + +:red-text:`Check 2:` no further message should arrive within 500 ms. + +:red-text:`Check 3:` a SET_SPATIAL_REQUIREMENTS for file_id = 0 should draw ERROR_DATA: + + - severity = DEBUG, tags = ["spatial"] + - message = "File id 0 not found" **Case 2: Duplicate Stokes type (Q + axis-degeneracy Q)** @@ -226,6 +253,123 @@ This test verifies that attempting to concatenate incompatible Stokes images ret {file: "IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits", polarizationType: 2} ] -:red-text:`Check 2:` the error response should contain: +:red-text:`Check 4:` the CONCAT_STOKES_FILES_ACK should satisfy: + + - success = False + - message contains "Duplicate Stokes type found!" + +:red-text:`Checks 5 and 6:` as Checks 2 and 3. + +**Case 3: A single file, too few to concatenate** + +3. Frontend sends: **CONCAT_STOKES_FILES** (``ConcatStokesFiles``) within 3000 ms + + .. code-block:: protobuf + + file_id = 0 + render_mode = RASTER + stokes_files = [ + {file: "IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits", polarizationType: 2} + ] + +:red-text:`Check 7:` the CONCAT_STOKES_FILES_ACK should satisfy: + + - success = False + - message contains "Need at least two files to concatenate!" + +:red-text:`Checks 8 and 9:` as Checks 2 and 3. + +**Case 4: Mixed file types (FITS + CASA image)** + +4. Frontend sends: **CONCAT_STOKES_FILES** (``ConcatStokesFiles``) within 3000 ms + + .. code-block:: protobuf + + file_id = 0 + render_mode = RASTER + stokes_files = [ + {file: "IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits", polarizationType: 2}, + {file: "M17_SWex.image", polarizationType: 3} + ] + +:red-text:`Check 10:` the CONCAT_STOKES_FILES_ACK should satisfy: + + - success = False + - message contains "Different file types can not be concatenated!" + +:red-text:`Checks 11 and 12:` as Checks 2 and 3. + +**Case 5: A hypercube with a gap in the Stokes axis (I, Q, V)** + +5. Frontend sends: **CONCAT_STOKES_FILES** (``ConcatStokesFiles``) within 3000 ms + + .. code-block:: protobuf + + file_id = 0 + render_mode = RASTER + stokes_files = [ + {file: "IRCp10216_sci.spw0.cube.I.manual.pbcor.fits", polarizationType: 1}, + {file: "IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits", polarizationType: 2}, + {file: "IRCp10216_sci.spw0.cube.V.manual.pbcor.fits", polarizationType: 4} + ] + +The Stokes types are checked for contiguity only when more than two files are given. I, Q and V map to +FITS Stokes values 1, 2 and 4, which are not evenly spaced, so the hypercube is refused by name. + +:red-text:`Check 13:` the CONCAT_STOKES_FILES_ACK should satisfy: + + - success = False + - message contains "Hypercube IQV is not allowed!" + +:red-text:`Checks 14 and 15:` as Checks 2 and 3. + +**Case 6: A file which is not on disk** + +6. Frontend sends: **CONCAT_STOKES_FILES** (``ConcatStokesFiles``) within 3000 ms + + .. code-block:: protobuf + + file_id = 0 + render_mode = RASTER + stokes_files = [ + {file: "IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits", polarizationType: 2}, + {file: "no_such_stokes_image.fits", polarizationType: 3} + ] + +:red-text:`Check 16:` the CONCAT_STOKES_FILES_ACK should satisfy: + + - success = False + - message contains "no_such_stokes_image.fits does not exist." + +:red-text:`Checks 17 and 18:` as Checks 2 and 3. + +**Case 7: A valid concatenation after the refusals (Q + U)** + +7. Frontend sends: **CONCAT_STOKES_FILES** (``ConcatStokesFiles``) within 3000 ms + + .. code-block:: protobuf + + file_id = 0 + render_mode = RASTER + stokes_files = [ + {file: "IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits", polarizationType: 2}, + {file: "IRCp10216_sci.spw0.cube.U.manual.pbcor.fits", polarizationType: 3} + ] + +Each refusal above opens image loaders before it gives up, and they are held until +``StokesFilesConnector::ClearCache`` runs. This case would be refused as a duplicate Stokes type if +that clean-up were skipped, so it also covers the recovery path. + +:red-text:`Check 19:` the CONCAT_STOKES_FILES_ACK and REGION_HISTOGRAM_DATA should satisfy: + + - success = True, openFileAck.success = True + - openFileAck.fileId = 0 + - openFileAck.fileInfo.name = "IRCp10216_sci.spw0.cube.hypercube_QU.manual.pbcor.fits" + - REGION_HISTOGRAM_DATA.fileId = 0 + +:red-text:`Check 20:` a SET_SPATIAL_REQUIREMENTS for file_id = 0 followed by a **SET_CURSOR** at +(128, 128) should now draw SPATIAL_PROFILE_DATA instead of the error of Check 3: - - Error message containing "Duplicate Stokes type found" + - fileId = 0, regionId = 0 + - x = 128, y = 128 + - the profile coordinates are ["x", "y"] as requested diff --git a/src/test/CONCAT_ERROR_MESSAGE.test.ts b/src/test/CONCAT_ERROR_MESSAGE.test.ts index 84451ee4..69b1ba0b 100644 --- a/src/test/CONCAT_ERROR_MESSAGE.test.ts +++ b/src/test/CONCAT_ERROR_MESSAGE.test.ts @@ -1,123 +1,322 @@ import { CARTA } from 'carta-protobuf'; import config from './config.json'; -import { checkConnection } from './MyClient'; +import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; let testServerUrl: string = config.serverURL0; let testSubdirectory: string = config.path.QA; let connectTimeout: number = config.timeout.connection; -let openFileTimeout = config.timeout.openFile; let concatStokeTimeout = config.timeout.concatStokes; +// CONCAT_STOKES_FILES_ACK is the only message a rejected concatenation is allowed to draw, and +// this is how long the backend is watched for a further one. +let quietTime: number = config.timeout.messageEvent; + +// Every rejection the backend can answer CONCAT_STOKES_FILES with is raised in +// StokesFilesConnector::OpenStokesFiles or StokesFilesConnector::StokesFilesValid and travels back +// in the message field of an unsuccessful CONCAT_STOKES_FILES_ACK. +interface ErrorCase { + title: string; + stokesFiles: CARTA.IStokesFile[]; + expectedError: string; +} interface AssertItem { - ConcatReqShape: CARTA.IConcatStokesFiles; - ConcatReqDeplicate: CARTA.IConcatStokesFiles; - ConcatResponse: string[]; + fileList: CARTA.IFileListRequest; + errorCases: ErrorCase[]; + validConcat: CARTA.IConcatStokesFiles; + validConcatName: string; + setCursor: { x: number; y: number }; + setSpatialReq: CARTA.ISetSpatialRequirements; + fileNotFoundError: { severity: CARTA.ErrorSeverity; tags: string[] }; } let assertItem: AssertItem = { - ConcatReqShape: { + fileList: { directory: testSubdirectory }, + errorCases: [ + { + title: 'Case 1: Q and axis-degeneracy U, image shapes inconsistent', + stokesFiles: [ + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + }, + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.U, + }, + ], + expectedError: 'Image shapes or axes are not consistent!', + }, + { + title: 'Case 2: Q and axis-degeneracy Q, duplicated Stokes type', + stokesFiles: [ + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + }, + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + }, + ], + expectedError: 'Duplicate Stokes type found!', + }, + { + title: 'Case 3: a single file, too few to concatenate', + stokesFiles: [ + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + }, + ], + expectedError: 'Need at least two files to concatenate!', + }, + { + title: 'Case 4: a FITS image and a CASA image, mixed file types', + stokesFiles: [ + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + }, + { + directory: testSubdirectory, + hdu: '', + file: 'M17_SWex.image', + polarizationType: CARTA.PolarizationType.U, + }, + ], + expectedError: 'Different file types can not be concatenated!', + }, + { + title: 'Case 5: I, Q and V, a hypercube with a gap in the Stokes axis', + stokesFiles: [ + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.I, + }, + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + }, + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.V, + }, + ], + expectedError: 'Hypercube IQV is not allowed!', + }, + { + title: 'Case 6: a file which is not on disk', + stokesFiles: [ + { + directory: testSubdirectory, + hdu: '', + file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + }, + { + directory: testSubdirectory, + hdu: '', + file: 'no_such_stokes_image.fits', + polarizationType: CARTA.PolarizationType.U, + }, + ], + expectedError: 'no_such_stokes_image.fits does not exist.', + }, + ], + validConcat: { fileId: 0, - renderMode: 0, + renderMode: CARTA.RenderMode.RASTER, stokesFiles: [ { directory: testSubdirectory, hdu: '', file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: 2, + polarizationType: CARTA.PolarizationType.Q, }, { directory: testSubdirectory, hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - polarizationType: 3, + file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.U, }, ], }, - ConcatReqDeplicate: { + validConcatName: 'IRCp10216_sci.spw0.cube.hypercube_QU.manual.pbcor.fits', + setCursor: { x: 128, y: 128 }, + setSpatialReq: { fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: 2, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', - polarizationType: 2, - }, + regionId: 0, + spatialProfiles: [ + { coordinate: 'x', mip: 1 }, + { coordinate: 'y', mip: 1 }, ], }, - ConcatResponse: ['are not consistent!', 'Duplicate Stokes type found'], + fileNotFoundError: { + severity: CARTA.ErrorSeverity.DEBUG, + tags: ['spatial'], + }, }; -describe('PERF_ANIMATION_PLAYBACK', () => { +let basepath: string; + +// The backend must have sent nothing beyond the CONCAT_STOKES_FILES_ACK itself. A concatenation +// which was refused must not go on to stream REGION_HISTOGRAM_DATA or raster data for an image it +// never opened. +async function assertOnlyTheAckArrived(messageCountBeforeRequest: number) { + const msgController = MessageController.Instance; + await new Promise((resolve) => setTimeout(resolve, quietTime)); + expect(msgController.messageReceiving()).toEqual(messageCountBeforeRequest + 1); +} + +// Session::OnSetSpatialRequirements answers a request naming a file the session holds no frame for +// with this error and nothing else. Since a refused concatenation is only visible in the ack, this +// is what shows that the file id it asked for was really left free: the frontend keeps its own file +// counter unchanged on a rejection (AppStore.loadConcatStokes), so a half-opened image on the +// backend would put the two out of step. +async function assertFileIdIsFree(fileId: number) { + const msgController = MessageController.Instance; + const errorDataStream = Stream(CARTA.ErrorData, 1); + msgController.setSpatialRequirements(assertItem.setSpatialReq); + const errorData = await errorDataStream; + expect(errorData[0].severity).toEqual(assertItem.fileNotFoundError.severity); + expect(errorData[0].tags).toEqual(assertItem.fileNotFoundError.tags); + expect(errorData[0].message).toEqual(`File id ${fileId} not found`); +} + +describe('CONCAT_ERROR_MESSAGE test: incompatible Stokes images are refused with a message', () => { const msgController = MessageController.Instance; beforeAll(async () => { await msgController.connect(testServerUrl); }, connectTimeout); checkConnection(); - let basepath: string; - describe(`Case 1: Q & axis-degeneracy U, Image shape inconsistent`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let fileListResponse = await msgController.getFileList('$BASE', 0); - basepath = fileListResponse.directory; + test(`Get the base path and prefix the Stokes file directories with it |`, async () => { + const fileListResponse = await msgController.getFileList('$BASE', 0); + basepath = fileListResponse.directory; + assertItem.fileList.directory = basepath + '/' + assertItem.fileList.directory; + assertItem.errorCases.forEach((errorCase) => { + errorCase.stokesFiles.forEach((stokesFile) => { + stokesFile.directory = basepath + '/' + stokesFile.directory; + }); + }); + assertItem.validConcat.stokesFiles!.forEach((stokesFile) => { + stokesFile.directory = basepath + '/' + stokesFile.directory; }); + }); - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqShape.stokesFiles.map((input, index) => { - assertItem.ConcatReqShape.stokesFiles[index].directory = testSubdirectory; - }); - msgController.closeFile(-1); - try { - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqShape.stokesFiles, - assertItem.ConcatReqShape.fileId, - assertItem.ConcatReqShape.renderMode - ); - } catch (err) { - expect(err).toContain(assertItem.ConcatResponse[0]); - } - }, - concatStokeTimeout - ); + // Case 6 asks for a file which is not on disk, so the test first has to know that the files the + // other cases name really are there. Otherwise every refusal below could be the same missing + // file error wearing a different case title. + test(`FILE_LIST_RESPONSE should list the Stokes images the cases are built from |`, async () => { + const fileListResponse = await msgController.getFileList(assertItem.fileList.directory!, 0); + const fileNames = fileListResponse.files!.map((file) => file.name); + assertItem.errorCases + .flatMap((errorCase) => errorCase.stokesFiles) + .concat(assertItem.validConcat.stokesFiles!) + .map((stokesFile) => stokesFile.file!) + .filter((file) => file !== 'no_such_stokes_image.fits') + .forEach((file) => expect(fileNames).toContain(file)); }); - describe(`Case 3: Q & axis-degeneracy Q, duplicated Stokes type`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let fileListResponse = await msgController.getFileList('$BASE', 0); - basepath = fileListResponse.directory; + assertItem.errorCases.forEach((errorCase) => { + describe(errorCase.title, () => { + let messageCountBeforeRequest: number; + + test( + `(Step 1) CONCAT_STOKES_FILES should be refused with "${errorCase.expectedError}" within ${concatStokeTimeout} ms | `, + async () => { + msgController.closeFile(-1); + messageCountBeforeRequest = msgController.messageReceiving(); + // loadStokeFiles rejects with the message field of an unsuccessful + // CONCAT_STOKES_FILES_ACK, so a request which is wrongly accepted fails here + // rather than passing unnoticed. + await expect( + msgController.loadStokeFiles( + errorCase.stokesFiles, + assertItem.validConcat.fileId!, + assertItem.validConcat.renderMode! + ) + ).rejects.toContain(errorCase.expectedError); + }, + concatStokeTimeout + ); + + test(`(Step 2) The refused CONCAT_STOKES_FILES should draw no other message | `, async () => { + await assertOnlyTheAckArrived(messageCountBeforeRequest); + }); + + test(`(Step 3) File id ${assertItem.validConcat.fileId} should have been left free | `, async () => { + await assertFileIdIsFree(assertItem.validConcat.fileId!); + }); }); + }); + + describe(`Case 7: Q & U after the refusals, a valid concatenation`, () => { + let concatStokesResponse: CARTA.IConcatStokesFilesAck; - let ConcatStokesResponse: any = []; test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, + `(Step 1) CONCAT_STOKES_FILES_ACK should arrive within ${concatStokeTimeout} ms | `, async () => { - assertItem.ConcatReqDeplicate.stokesFiles.map((input, index) => { - assertItem.ConcatReqDeplicate.stokesFiles[index].directory = testSubdirectory; - }); msgController.closeFile(-1); - try { - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqDeplicate.stokesFiles, - assertItem.ConcatReqDeplicate.fileId, - assertItem.ConcatReqDeplicate.renderMode - ); - } catch (err) { - expect(err).toContain(assertItem.ConcatResponse[1]); - } + const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); + concatStokesResponse = await msgController.loadStokeFiles( + assertItem.validConcat.stokesFiles!, + assertItem.validConcat.fileId!, + assertItem.validConcat.renderMode! + ); + const regionHistogramData = await regionHistogramDataStream; + expect(regionHistogramData[0].fileId).toEqual(assertItem.validConcat.fileId); }, concatStokeTimeout ); + + // The refusals above each leave the connector holding the loaders they opened until + // StokesFilesConnector::ClearCache runs. If that were skipped, the Stokes types of a failed + // attempt would still be registered and this concatenation would be refused as a duplicate. + test(`(Step 2) The concatenated image should be opened under file id ${assertItem.validConcat.fileId} | `, () => { + expect(concatStokesResponse.success).toBe(true); + expect(concatStokesResponse.openFileAck!.success).toBe(true); + expect(concatStokesResponse.openFileAck!.fileId).toEqual(assertItem.validConcat.fileId); + expect(concatStokesResponse.openFileAck!.fileInfo!.name).toEqual(assertItem.validConcatName); + }); + + // The same request which drew "File id 0 not found" from every refused case now draws a + // profile instead, which is the other half of what those cases assert. Session only fills a + // cursor profile once the cursor has been placed, so the requirements are registered first + // and the cursor is what triggers the stream. + test(`(Step 3) SPATIAL_PROFILE_DATA should be streamed for the concatenated image | `, async () => { + msgController.setSpatialRequirements(assertItem.setSpatialReq); + const spatialProfileDataStream = Stream(CARTA.SpatialProfileData, 1); + msgController.setCursor(assertItem.validConcat.fileId!, assertItem.setCursor.x, assertItem.setCursor.y); + const spatialProfileData = await spatialProfileDataStream; + expect(spatialProfileData[0].fileId).toEqual(assertItem.validConcat.fileId); + expect(spatialProfileData[0].regionId).toEqual(assertItem.setSpatialReq.regionId); + expect(spatialProfileData[0].x).toEqual(assertItem.setCursor.x); + expect(spatialProfileData[0].y).toEqual(assertItem.setCursor.y); + expect(spatialProfileData[0].profiles!.map((profile) => profile.coordinate)).toEqual( + assertItem.setSpatialReq.spatialProfiles!.map((profile) => profile.coordinate) + ); + }); }); afterAll(() => msgController.closeConnection()); From 9669069349d75e50b27d7c7c3aeae246ed46a2f9 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 19 Aug 2026 11:08:23 +0800 Subject: [PATCH 2/7] Check every Stokes plane of the concatenated hypercube --- ICD_test_stages/file_browser.tests | 1 + docs/source/concat_stokes.rst | 94 ++- src/test/CONCAT_STOKES_IMAGES.test.ts | 992 ++++++-------------------- 3 files changed, 280 insertions(+), 807 deletions(-) diff --git a/ICD_test_stages/file_browser.tests b/ICD_test_stages/file_browser.tests index affe0efa..93151bd0 100644 --- a/ICD_test_stages/file_browser.tests +++ b/ICD_test_stages/file_browser.tests @@ -3,4 +3,5 @@ src/test/GET_FILELIST_ROOTPATH_CONCURRENT.test.ts src/test/FILEINFO_FITS_MULTIHDU.test.ts src/test/FILEINFO_EXCEPTIONS.test.ts src/test/OPEN_SWAPPED_IMAGES.test.ts +src/test/CONCAT_STOKES_IMAGES.test.ts src/test/CONCAT_ERROR_MESSAGE.test.ts diff --git a/docs/source/concat_stokes.rst b/docs/source/concat_stokes.rst index 0a38bb29..318a71c0 100644 --- a/docs/source/concat_stokes.rst +++ b/docs/source/concat_stokes.rst @@ -34,7 +34,7 @@ CONCAT_STOKES_IMAGES See the `source code `__. -This test verifies that separate Stokes parameter images (I, Q, U, V) stored as individual 4D FITS cubes (256x256x480) can be concatenated into a single hypercube image. It tests 5 different Stokes combinations and checks both the ``CONCAT_STOKES_FILES_ACK`` and ``REGION_HISTOGRAM_DATA`` responses. +This test verifies that separate Stokes parameter images (I, Q, U, V) stored as individual 4D FITS cubes (256x256x480) can be concatenated into a single hypercube image, and that each plane of that hypercube holds the image which went into it. It tests 5 different Stokes combinations. **Input files** (from ``set_QA`` directory): @@ -43,9 +43,42 @@ This test verifies that separate Stokes parameter images (I, Q, U, V) stored as - ``IRCp10216_sci.spw0.cube.U.manual.pbcor.fits`` (polarizationType = 3) - ``IRCp10216_sci.spw0.cube.V.manual.pbcor.fits`` (polarizationType = 4) -**For each case, the test performs 3 steps:** +Opened on its own, each of these four cubes answers with a distinct **REGION_HISTOGRAM_DATA**, and those four histograms are the reference the test identifies the hypercube's planes by: -1. Frontend sends: **FILE_LIST_REQUEST** and verifies the file list is returned successfully. +.. list-table:: + :header-rows: 1 + + * - Stokes + - binWidth + - firstBinCenter + - mean + - stdDev + * - I + - 0.004779201466590166 + - -0.11032065749168396 + - 0.0014072911570091893 + - 0.05368401551544901 + * - Q + - 0.00016267175669781864 + - -0.018377140164375305 + - -0.00003742659352908538 + - 0.003869341538017443 + * - U + - 0.00016493673319928348 + - -0.02082323282957077 + - 0.00012091044507226787 + - 0.004009951489450122 + * - V + - 0.00016941891226451844 + - -0.020163865759968758 + - 0.000017799031213005305 + - 0.003931388177191896 + +``StokesFilesConnector`` iterates its loaders in polarization order, so the Stokes axis it builds is sorted I, Q, U, V however the request was ordered. Every case below therefore names its files in **reverse** order, and expects the sorted axis back. + +**For each case, the test performs 5 steps:** + +1. Frontend sends: **FILE_LIST_REQUEST** and verifies the input images are listed. 2. Frontend sends: **FILE_INFO_REQUEST** (``FileInfoRequest``) for each Stokes file to be combined @@ -56,7 +89,8 @@ This test verifies that separate Stokes parameter images (I, Q, U, V) stored as :red-text:`Check 1:` each FILE_INFO_RESPONSE should satisfy: - - FILE_INFO_RESPONSE.success = True + - success = True + - fileInfo.name = the file which was asked for 3. Frontend sends: **CONCAT_STOKES_FILES** (``ConcatStokesFiles``) within 3000 ms @@ -66,28 +100,38 @@ This test verifies that separate Stokes parameter images (I, Q, U, V) stored as render_mode = RASTER stokes_files = [{directory, hdu, file, polarizationType}, ...] -4. Backend returns: **CONCAT_STOKES_FILES_ACK** (``ConcatStokesFilesAck``) and **REGION_HISTOGRAM_DATA** (``RegionHistogramData``) +4. Backend returns: **CONCAT_STOKES_FILES_ACK** (``ConcatStokesFilesAck``) and **REGION_HISTOGRAM_DATA** (``RegionHistogramData``) for the plane the hypercube opens on, which is Stokes plane 0. -**Case 1: Combine I, Q, U, V** +5. Frontend sends: **SET_IMAGE_CHANNELS** (``SetImageChannels``) once per remaining plane, which is what the Stokes selector sends, and the backend answers each with a **REGION_HISTOGRAM_DATA** for that plane + + .. code-block:: protobuf + + file_id = 0 + channel = 0 + stokes = 1 .. (number of planes - 1) + required_tiles = {file_id: 0, tiles: [0], compression_type: ZFP, compression_quality: 11} + +Every REGION_HISTOGRAM_DATA, whether it came with the ack or from a **SET_IMAGE_CHANNELS**, is checked to carry fileId = 0, regionId = -1, progress = 1, config.numBins = -1, histograms.numBins = 256, and the four histogram values of the Stokes image which belongs on that plane. The beam table is checked against the shape rather than a constant: ``DoConcat`` rebuilds it as one beam per channel per plane, so its length must be 480 × the number of planes. + +**Case 1: Combine I, Q, U, V** (requested V, U, Q, I) :red-text:`Check 2:` the CONCAT_STOKES_FILES_ACK should satisfy: - success = True - openFileAck.success = True + - openFileAck.fileId = 0 - openFileAck.fileInfo.name = "IRCp10216_sci.spw0.cube.hypercube_IQUV.manual.pbcor.fits" - openFileAck.fileInfoExtended: dimensions = 4, width = 256, height = 256, depth = 480, stokes = 4 - openFileAck.beamTable.length = 1920 -:red-text:`Check 3:` the REGION_HISTOGRAM_DATA should satisfy: +:red-text:`Check 3:` the Stokes axis should read I, Q, U, V: - - regionId = -1, progress = 1, config.numBins = -1 - - histograms.binWidth = 0.004779201466590166 - - histograms.firstBinCenter = -0.11032065749168396 - - histograms.numBins = 256 - - histograms.mean = 0.0014072911570091893 - - histograms.stdDev = 0.05368401551544911 + - plane 0 (the histogram which came with the ack) matches Stokes I + - plane 1 matches Stokes Q + - plane 2 matches Stokes U + - plane 3 matches Stokes V -**Case 2: Combine I, V** +**Case 2: Combine I, V** (requested V, I) :red-text:`Check 4:` the CONCAT_STOKES_FILES_ACK should satisfy: @@ -96,9 +140,9 @@ This test verifies that separate Stokes parameter images (I, Q, U, V) stored as - openFileAck.fileInfoExtended: dimensions = 4, width = 256, height = 256, depth = 480, stokes = 2 - openFileAck.beamTable.length = 960 -:red-text:`Check 5:` the REGION_HISTOGRAM_DATA should have the same values as Check 3 (Stokes I is the first component). +:red-text:`Check 5:` the Stokes axis should read I, V — plane 0 matches Stokes I, plane 1 matches Stokes V. -**Case 3: Combine Q, U** +**Case 3: Combine Q, U** (requested U, Q) :red-text:`Check 6:` the CONCAT_STOKES_FILES_ACK should satisfy: @@ -107,15 +151,9 @@ This test verifies that separate Stokes parameter images (I, Q, U, V) stored as - openFileAck.fileInfoExtended: dimensions = 4, width = 256, height = 256, depth = 480, stokes = 2 - openFileAck.beamTable.length = 960 -:red-text:`Check 7:` the REGION_HISTOGRAM_DATA should satisfy: - - - histograms.binWidth = 0.00016267175669781864 - - histograms.firstBinCenter = -0.018377140164375305 - - histograms.numBins = 256 - - histograms.mean = -0.00003742659352908538 - - histograms.stdDev = 0.0038693415380174558 +:red-text:`Check 7:` the Stokes axis should read Q, U — plane 0 matches Stokes Q, plane 1 matches Stokes U. -**Case 4: Combine I, Q, U** +**Case 4: Combine I, Q, U** (requested U, Q, I) :red-text:`Check 8:` the CONCAT_STOKES_FILES_ACK should satisfy: @@ -124,9 +162,9 @@ This test verifies that separate Stokes parameter images (I, Q, U, V) stored as - openFileAck.fileInfoExtended: dimensions = 4, width = 256, height = 256, depth = 480, stokes = 3 - openFileAck.beamTable.length = 1440 -:red-text:`Check 9:` the REGION_HISTOGRAM_DATA should have the same values as Check 3 (Stokes I is the first component). +:red-text:`Check 9:` the Stokes axis should read I, Q, U. -**Case 5: Combine Q, U, V** +**Case 5: Combine Q, U, V** (requested V, U, Q) :red-text:`Check 10:` the CONCAT_STOKES_FILES_ACK should satisfy: @@ -135,14 +173,14 @@ This test verifies that separate Stokes parameter images (I, Q, U, V) stored as - openFileAck.fileInfoExtended: dimensions = 4, width = 256, height = 256, depth = 480, stokes = 3 - openFileAck.beamTable.length = 1440 -:red-text:`Check 11:` the REGION_HISTOGRAM_DATA should have the same values as Check 7 (Stokes Q is the first component). +:red-text:`Check 11:` the Stokes axis should read Q, U, V. CONCAT_STOKES_IMAGES_AXIS_DEGENERACY ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ See the `source code `__. -This test is identical in structure and expected results to CONCAT_STOKES_IMAGES, but uses axis-degeneracy-dropped ("dropdeg") versions of the Stokes images. This verifies that the backend correctly handles images where degenerate axes have been removed. +This test covers the same 5 combinations as CONCAT_STOKES_IMAGES, but uses axis-degeneracy-dropped ("dropdeg") versions of the Stokes images. This verifies that the backend correctly handles images where degenerate axes have been removed. It checks only the first Stokes plane of each hypercube; it does not walk the Stokes axis the way CONCAT_STOKES_IMAGES does. **Input files** (from ``set_QA`` directory): diff --git a/src/test/CONCAT_STOKES_IMAGES.test.ts b/src/test/CONCAT_STOKES_IMAGES.test.ts index 63a6c169..85f57e14 100644 --- a/src/test/CONCAT_STOKES_IMAGES.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES.test.ts @@ -1,6 +1,6 @@ import { CARTA } from 'carta-protobuf'; import config from './config.json'; -import { checkConnection } from './MyClient'; +import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; let testServerUrl: string = config.serverURL0; @@ -8,322 +8,154 @@ let testSubdirectory: string = config.path.QA; let connectTimeout: number = config.timeout.connection; let openFileTimeout = config.timeout.openFile; let concatStokeTimeout = config.timeout.concatStokes; +let changeChannelTimeout = config.timeout.changeChannel; -interface ConcatStokesFilesAckExt extends CARTA.IConcatStokesFilesAck { - OpenFileAckBeamLength: number; +type StokesLetter = 'I' | 'Q' | 'U' | 'V'; + +// One Stokes cube, and the REGION_HISTOGRAM_DATA the backend answers with when that cube is opened +// on its own. The four histograms are what tells the planes of a hypercube apart, so the test can +// say which image ended up on which Stokes plane rather than only counting the planes. +interface StokesImage { + file: string; + polarizationType: CARTA.PolarizationType; + histogram: CARTA.IHistogram; +} + +interface ConcatCase { + title: string; + // StokesFilesConnector iterates its loaders in polarization order, so the Stokes axis it builds + // is sorted no matter how the request was ordered. Every case is requested out of order to hold + // the backend to that. + requestOrder: StokesLetter[]; + expectedPlanes: StokesLetter[]; + hypercubeName: string; } interface AssertItem { - filelist: CARTA.IFileListRequest; - fileInfoReq: CARTA.IFileInfoRequest[]; - ConcatReq: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseIQUV: CARTA.IRegionHistogramData; - ConcatReqIV: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseIV: CARTA.IRegionHistogramData; - ConcatReqQU: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseQU: CARTA.IRegionHistogramData; - ConcatReqIQU: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseIQU: CARTA.IRegionHistogramData; - ConcatReqQUV: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseQUV: CARTA.IRegionHistogramData; - ConcatResponse: ConcatStokesFilesAckExt[]; + fileList: CARTA.IFileListRequest; + stokesImages: Record; + concatCases: ConcatCase[]; + fileId: number; + renderMode: CARTA.RenderMode; + imageShape: { dimensions: number; width: number; height: number; depth: number }; + regionHistogram: { regionId: number; progress: number; numBins: number; histogramNumBins: number }; + requiredTiles: CARTA.IAddRequiredTiles; precisionDigits: number; } let assertItem: AssertItem = { - filelist: { directory: testSubdirectory }, - fileInfoReq: [ - { + fileList: { directory: testSubdirectory }, + stokesImages: { + I: { file: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', - hdu: '', - }, - { - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - hdu: '', - }, - { - file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', - hdu: '', - }, - { - file: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', - hdu: '', - }, - ], - ConcatReq: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', - polarizationType: 4, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', - polarizationType: 3, + polarizationType: CARTA.PolarizationType.I, + histogram: { + binWidth: 0.004779201466590166, + firstBinCenter: -0.11032065749168396, + mean: 0.0014072911570091893, + stdDev: 0.05368401551544901, }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: 2, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', - polarizationType: 1, - }, - ], - }, - RegionHistogramDataResponseIQUV: { - progress: 1, - regionId: -1, - config: { - numBins: -1, }, - histograms: { - binWidth: 0.004779201466590166, - firstBinCenter: -0.11032065749168396, - numBins: 256, - stdDev: 0.05368401551544911, - mean: 0.0014072911570091893, - }, - }, - ConcatReqIV: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', - polarizationType: 4, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', - polarizationType: 1, - }, - ], - }, - RegionHistogramDataResponseIV: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.004779201466590166, - firstBinCenter: -0.11032065749168396, - numBins: 256, - stdDev: 0.05368401551544911, - mean: 0.0014072911570091893, - }, - }, - ConcatReqQU: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', - polarizationType: 3, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: 2, + Q: { + file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + histogram: { + binWidth: 0.00016267175669781864, + firstBinCenter: -0.018377140164375305, + mean: -0.00003742659352908538, + stdDev: 0.003869341538017443, }, - ], - }, - RegionHistogramDataResponseQU: { - progress: 1, - regionId: -1, - config: { - numBins: -1, }, - histograms: { - binWidth: 0.00016267175669781864, - firstBinCenter: -0.018377140164375305, - numBins: 256, - stdDev: 0.0038693415380174558, - mean: -0.00003742659352908538, - }, - }, - ConcatReqIQU: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', - polarizationType: 3, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: 2, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', - polarizationType: 1, + U: { + file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.U, + histogram: { + binWidth: 0.00016493673319928348, + firstBinCenter: -0.02082323282957077, + mean: 0.00012091044507226787, + stdDev: 0.004009951489450122, }, - ], - }, - RegionHistogramDataResponseIQU: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.004779201466590166, - firstBinCenter: -0.11032065749168396, - numBins: 256, - stdDev: 0.05368401551544911, - mean: 0.0014072911570091893, }, - }, - ConcatReqQUV: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', - polarizationType: 4, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', - polarizationType: 3, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: 2, + V: { + file: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.V, + histogram: { + binWidth: 0.00016941891226451844, + firstBinCenter: -0.020163865759968758, + mean: 0.000017799031213005305, + stdDev: 0.003931388177191896, }, - ], - }, - RegionHistogramDataResponseQUV: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.00016267175669781864, - firstBinCenter: -0.018377140164375305, - numBins: 256, - stdDev: 0.0038693415380174558, - mean: -0.00003742659352908538, }, }, - ConcatResponse: [ + concatCases: [ { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_IQUV.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 4, - width: 256, - }, - }, - OpenFileAckBeamLength: 1920, + title: 'Case 1: Combine I, Q, U & V', + requestOrder: ['V', 'U', 'Q', 'I'], + expectedPlanes: ['I', 'Q', 'U', 'V'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_IQUV.manual.pbcor.fits', }, { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_IV.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 2, - width: 256, - }, - }, - OpenFileAckBeamLength: 960, + title: 'Case 2: Combine I & V', + requestOrder: ['V', 'I'], + expectedPlanes: ['I', 'V'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_IV.manual.pbcor.fits', }, { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_QU.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 2, - width: 256, - }, - }, - OpenFileAckBeamLength: 960, + title: 'Case 3: Combine Q & U', + requestOrder: ['U', 'Q'], + expectedPlanes: ['Q', 'U'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_QU.manual.pbcor.fits', }, { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_IQU.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 3, - width: 256, - }, - }, - OpenFileAckBeamLength: 1440, + title: 'Case 4: Combine I, Q & U', + requestOrder: ['U', 'Q', 'I'], + expectedPlanes: ['I', 'Q', 'U'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_IQU.manual.pbcor.fits', }, { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_QUV.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 3, - width: 256, - }, - }, - OpenFileAckBeamLength: 1440, + title: 'Case 5: Combine Q, U & V', + requestOrder: ['V', 'U', 'Q'], + expectedPlanes: ['Q', 'U', 'V'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_QUV.manual.pbcor.fits', }, ], + fileId: 0, + renderMode: CARTA.RenderMode.RASTER, + // Every input cube has this shape, and concatenation only adds the Stokes axis to it. + imageShape: { dimensions: 4, width: 256, height: 256, depth: 480 }, + regionHistogram: { regionId: -1, progress: 1, numBins: -1, histogramNumBins: 256 }, + requiredTiles: { + fileId: 0, + tiles: [0], + compressionType: CARTA.CompressionType.ZFP, + compressionQuality: 11, + }, precisionDigits: 6, }; +let basepath: string; + +// The whole point of a hypercube is that the plane the viewer asks for is the image which went into +// it, so each plane is compared against the histogram of the single-Stokes cube it came from. +function assertHistogramMatchesStokesImage( + regionHistogramData: CARTA.IRegionHistogramData, + stokesLetter: StokesLetter +) { + const expected = assertItem.stokesImages[stokesLetter].histogram; + expect(regionHistogramData.fileId).toEqual(assertItem.fileId); + expect(regionHistogramData.regionId).toEqual(assertItem.regionHistogram.regionId); + expect(regionHistogramData.progress).toEqual(assertItem.regionHistogram.progress); + expect(regionHistogramData.config!.numBins).toEqual(assertItem.regionHistogram.numBins); + expect(regionHistogramData.histograms!.numBins).toEqual(assertItem.regionHistogram.histogramNumBins); + expect(regionHistogramData.histograms!.binWidth).toBeCloseTo(expected.binWidth!, assertItem.precisionDigits); + expect(regionHistogramData.histograms!.firstBinCenter).toBeCloseTo( + expected.firstBinCenter!, + assertItem.precisionDigits + ); + expect(regionHistogramData.histograms!.mean).toBeCloseTo(expected.mean!, assertItem.precisionDigits); + expect(regionHistogramData.histograms!.stdDev).toBeCloseTo(expected.stdDev!, assertItem.precisionDigits); +} + describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into single image', () => { const msgController = MessageController.Instance; beforeAll(async () => { @@ -332,507 +164,109 @@ describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into si checkConnection(); - describe(`Case 1: Combine I,Q,U,V |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); - - assertItem.fileInfoReq.map((input, index) => { - test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, - async () => { - let FileInfoResponse = await msgController.getFileInfo(testSubdirectory, input.file, input.hdu); - expect(FileInfoResponse.success).toEqual(true); - }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReq.stokesFiles.map((input, index) => { - assertItem.ConcatReq.stokesFiles[index].directory = testSubdirectory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReq.stokesFiles, - assertItem.ConcatReq.fileId, - assertItem.ConcatReq.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseIQUV.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseIQUV.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseIQUV.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQUV.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQUV.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQUV.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseIQUV.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQUV.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[0].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[0].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[0].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.depth - ); - }); + test(`Get the base path and prefix the image directory with it |`, async () => { + const fileListResponse = await msgController.getFileList('$BASE', 0); + basepath = fileListResponse.directory; + assertItem.fileList.directory = basepath + '/' + assertItem.fileList.directory; }); - describe(`Case 2: Combine I & V |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); + assertItem.concatCases.forEach((concatCase) => { + describe(`${concatCase.title} |`, () => { + let concatStokesResponse: CARTA.IConcatStokesFilesAck; + + test(`(Step 1) FILE_LIST_RESPONSE should list the input images |`, async () => { + const fileListResponse = await msgController.getFileList(assertItem.fileList.directory!, 0); + expect(fileListResponse.success).toEqual(true); + const fileNames = fileListResponse.files!.map((file) => file.name); + concatCase.requestOrder.forEach((stokesLetter) => + expect(fileNames).toContain(assertItem.stokesImages[stokesLetter].file) + ); + }); + + concatCase.requestOrder.forEach((stokesLetter, index) => { + test( + `(Step 2-${index + 1}) FILE_INFO_RESPONSE for the Stokes ${stokesLetter} cube should arrive within ${openFileTimeout} ms | `, + async () => { + const stokesImage = assertItem.stokesImages[stokesLetter]; + const fileInfoResponse = await msgController.getFileInfo( + assertItem.fileList.directory!, + stokesImage.file, + '' + ); + expect(fileInfoResponse.success).toEqual(true); + expect(fileInfoResponse.fileInfo!.name).toEqual(stokesImage.file); + }, + openFileTimeout + ); + }); - let FileInfoResponse: any = []; - let inputIndex = [0, 3]; - inputIndex.map((input, index) => { test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, + `(Step 3) CONCAT_STOKES_FILES_ACK and REGION_HISTOGRAM_DATA should arrive within ${concatStokeTimeout} ms | `, async () => { - FileInfoResponse = await msgController.getFileInfo( - testSubdirectory, - assertItem.fileInfoReq[input].file, - assertItem.fileInfoReq[input].hdu + msgController.closeFile(-1); + const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); + concatStokesResponse = await msgController.loadStokeFiles( + concatCase.requestOrder.map((stokesLetter) => ({ + directory: assertItem.fileList.directory, + hdu: '', + file: assertItem.stokesImages[stokesLetter].file, + polarizationType: assertItem.stokesImages[stokesLetter].polarizationType, + })), + assertItem.fileId, + assertItem.renderMode ); - expect(FileInfoResponse.success).toEqual(true); + const regionHistogramData = await regionHistogramDataStream; + // The hypercube opens on its first plane, which is the lowest polarization of + // the set rather than the one which happened to be requested first. + expect(regionHistogramData[0].stokes).toEqual(0); + assertHistogramMatchesStokesImage(regionHistogramData[0], concatCase.expectedPlanes[0]); }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqIV.stokesFiles.map((input, index) => { - assertItem.ConcatReqIV.stokesFiles[index].directory = testSubdirectory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqIV.stokesFiles, - assertItem.ConcatReqIV.fileId, - assertItem.ConcatReqIV.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseIV.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseIV.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseIV.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseIV.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseIV.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseIV.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseIV.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseIV.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[1].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[1].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[1].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.depth - ); - }); - }); - - describe(`Case 3: Combine Q & U |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); - - let FileInfoResponse: any = []; - let inputIndex = [1, 2]; - inputIndex.map((input, index) => { - test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, - async () => { - FileInfoResponse = await msgController.getFileInfo( - testSubdirectory, - assertItem.fileInfoReq[input].file, - assertItem.fileInfoReq[input].hdu - ); - expect(FileInfoResponse.success).toEqual(true); - }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqQU.stokesFiles.map((input, index) => { - assertItem.ConcatReqQU.stokesFiles[index].directory = testSubdirectory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqQU.stokesFiles, - assertItem.ConcatReqQU.fileId, - assertItem.ConcatReqQU.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseQU.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseQU.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseQU.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseQU.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseQU.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseQU.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseQU.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseQU.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[2].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[2].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[2].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.depth - ); - }); - }); - - describe(`Case 4: Combine I, Q & U |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); - - let FileInfoResponse: any = []; - let inputIndex = [0, 1, 2]; - inputIndex.map((input, index) => { - test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, - async () => { - FileInfoResponse = await msgController.getFileInfo( - testSubdirectory, - assertItem.fileInfoReq[input].file, - assertItem.fileInfoReq[input].hdu - ); - expect(FileInfoResponse.success).toEqual(true); - }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqIQU.stokesFiles.map((input, index) => { - assertItem.ConcatReqIQU.stokesFiles[index].directory = testSubdirectory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqIQU.stokesFiles, - assertItem.ConcatReqIQU.fileId, - assertItem.ConcatReqIQU.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseIQU.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseIQU.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseIQU.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQU.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQU.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQU.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseIQU.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQU.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[3].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[3].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[3].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.depth - ); - }); - }); - - describe(`Case 5: Combine Q, U & V |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); - - let FileInfoResponse: any = []; - let inputIndex = [1, 2, 3]; - inputIndex.map((input, index) => { - test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, - async () => { - FileInfoResponse = await msgController.getFileInfo( - testSubdirectory, - assertItem.fileInfoReq[input].file, - assertItem.fileInfoReq[input].hdu - ); - expect(FileInfoResponse.success).toEqual(true); - }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqQUV.stokesFiles.map((input, index) => { - assertItem.ConcatReqQUV.stokesFiles[index].directory = testSubdirectory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqQUV.stokesFiles, - assertItem.ConcatReqQUV.fileId, - assertItem.ConcatReqQUV.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseQUV.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseQUV.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseQUV.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseQUV.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseQUV.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseQUV.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseQUV.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseQUV.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[4].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[4].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[4].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.depth - ); + concatStokeTimeout + ); + + test(`(Step 4) CONCAT_STOKES_FILES_ACK should describe the hypercube | `, () => { + const openFileAck = concatStokesResponse.openFileAck!; + const fileInfoExtended = openFileAck.fileInfoExtended!; + expect(concatStokesResponse.success).toEqual(true); + expect(openFileAck.success).toEqual(true); + expect(openFileAck.fileId).toEqual(assertItem.fileId); + expect(openFileAck.fileInfo!.name).toEqual(concatCase.hypercubeName); + expect(fileInfoExtended.dimensions).toEqual(assertItem.imageShape.dimensions); + expect(fileInfoExtended.width).toEqual(assertItem.imageShape.width); + expect(fileInfoExtended.height).toEqual(assertItem.imageShape.height); + expect(fileInfoExtended.depth).toEqual(assertItem.imageShape.depth); + expect(fileInfoExtended.stokes).toEqual(concatCase.expectedPlanes.length); + // StokesFilesConnector::DoConcat rebuilds the beam table as one beam per channel per + // Stokes plane, so its length is the shape of the hypercube restated. + expect(openFileAck.beamTable!.length).toEqual( + assertItem.imageShape.depth * concatCase.expectedPlanes.length + ); + }); + + // Plane 0 was covered by the histogram which came with the ack. The remaining planes are + // reached with SET_IMAGE_CHANNELS, which is what the Stokes selector in the frontend + // sends. A concatenation which put the images on the wrong planes, or which silently + // repeated one of them, fails here and nowhere else. + concatCase.expectedPlanes.slice(1).forEach((stokesLetter, index) => { + const stokes = index + 1; + test( + `(Step 5-${stokes}) Stokes plane ${stokes} should hold the ${stokesLetter} image within ${changeChannelTimeout} ms | `, + async () => { + const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); + msgController.setChannels({ + fileId: assertItem.fileId, + channel: 0, + stokes: stokes, + requiredTiles: assertItem.requiredTiles, + }); + const regionHistogramData = await regionHistogramDataStream; + expect(regionHistogramData[0].stokes).toEqual(stokes); + expect(regionHistogramData[0].channel).toEqual(0); + assertHistogramMatchesStokesImage(regionHistogramData[0], stokesLetter); + }, + changeChannelTimeout + ); + }); }); }); From 863014793694011d13db08ae543b219c9642d948 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 19 Aug 2026 11:14:52 +0800 Subject: [PATCH 3/7] Resolve the Stokes image directories against the base path --- ICD_test_stages/file_browser.tests | 1 + ...NCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts | 32 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/ICD_test_stages/file_browser.tests b/ICD_test_stages/file_browser.tests index 93151bd0..868cb2e7 100644 --- a/ICD_test_stages/file_browser.tests +++ b/ICD_test_stages/file_browser.tests @@ -4,4 +4,5 @@ src/test/FILEINFO_FITS_MULTIHDU.test.ts src/test/FILEINFO_EXCEPTIONS.test.ts src/test/OPEN_SWAPPED_IMAGES.test.ts src/test/CONCAT_STOKES_IMAGES.test.ts +src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts src/test/CONCAT_ERROR_MESSAGE.test.ts diff --git a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts index cbb20e88..5f948259 100644 --- a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts @@ -322,6 +322,8 @@ let assertItem: AssertItem = { ], }; +let basepath: string; + describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis-degeneracy stokes images into a single image.', () => { const msgController = MessageController.Instance; beforeAll(async () => { @@ -330,6 +332,12 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- checkConnection(); + test(`Get the base path and prefix the image directory with it |`, async () => { + const fileListResponse = await msgController.getFileList('$BASE', 0); + basepath = fileListResponse.directory; + assertItem.filelist.directory = basepath + '/' + assertItem.filelist.directory; + }); + describe(`Case 1: Combine I,Q,U,V |`, () => { test(`(Step 1) Assert FileListRequest |`, async () => { let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); @@ -340,7 +348,11 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- test( `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, async () => { - let FileInfoResponse = await msgController.getFileInfo(testSubdirectory, input.file, input.hdu); + let FileInfoResponse = await msgController.getFileInfo( + assertItem.filelist.directory, + input.file, + input.hdu + ); expect(FileInfoResponse.success).toEqual(true); }, openFileTimeout @@ -352,7 +364,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, async () => { assertItem.ConcatReq.stokesFiles.map((input, index) => { - assertItem.ConcatReq.stokesFiles[index].directory = testSubdirectory; + assertItem.ConcatReq.stokesFiles[index].directory = assertItem.filelist.directory; }); msgController.closeFile(-1); let regionHistogramDataArray = []; @@ -439,7 +451,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, async () => { FileInfoResponse = await msgController.getFileInfo( - testSubdirectory, + assertItem.filelist.directory, assertItem.fileInfoReq[input].file, assertItem.fileInfoReq[input].hdu ); @@ -454,7 +466,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, async () => { assertItem.ConcatReqIV.stokesFiles.map((input, index) => { - assertItem.ConcatReqIV.stokesFiles[index].directory = testSubdirectory; + assertItem.ConcatReqIV.stokesFiles[index].directory = assertItem.filelist.directory; }); msgController.closeFile(-1); let regionHistogramDataArray = []; @@ -541,7 +553,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, async () => { FileInfoResponse = await msgController.getFileInfo( - testSubdirectory, + assertItem.filelist.directory, assertItem.fileInfoReq[input].file, assertItem.fileInfoReq[input].hdu ); @@ -556,7 +568,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, async () => { assertItem.ConcatReqQU.stokesFiles.map((input, index) => { - assertItem.ConcatReqQU.stokesFiles[index].directory = testSubdirectory; + assertItem.ConcatReqQU.stokesFiles[index].directory = assertItem.filelist.directory; }); msgController.closeFile(-1); let regionHistogramDataArray = []; @@ -643,7 +655,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, async () => { FileInfoResponse = await msgController.getFileInfo( - testSubdirectory, + assertItem.filelist.directory, assertItem.fileInfoReq[input].file, assertItem.fileInfoReq[input].hdu ); @@ -658,7 +670,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, async () => { assertItem.ConcatReqIQU.stokesFiles.map((input, index) => { - assertItem.ConcatReqIQU.stokesFiles[index].directory = testSubdirectory; + assertItem.ConcatReqIQU.stokesFiles[index].directory = assertItem.filelist.directory; }); msgController.closeFile(-1); let regionHistogramDataArray = []; @@ -745,7 +757,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, async () => { FileInfoResponse = await msgController.getFileInfo( - testSubdirectory, + assertItem.filelist.directory, assertItem.fileInfoReq[input].file, assertItem.fileInfoReq[input].hdu ); @@ -760,7 +772,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, async () => { assertItem.ConcatReqQUV.stokesFiles.map((input, index) => { - assertItem.ConcatReqQUV.stokesFiles[index].directory = testSubdirectory; + assertItem.ConcatReqQUV.stokesFiles[index].directory = assertItem.filelist.directory; }); msgController.closeFile(-1); let regionHistogramDataArray = []; From f2cdcbd5d50ce9453d77324e65ce31c809a39e33 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 19 Aug 2026 11:21:54 +0800 Subject: [PATCH 4/7] Check every Stokes plane of the axis-degeneracy hypercube --- docs/source/concat_stokes.rst | 25 +- ...NCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts | 1010 ++++------------- 2 files changed, 240 insertions(+), 795 deletions(-) diff --git a/docs/source/concat_stokes.rst b/docs/source/concat_stokes.rst index 318a71c0..d4666472 100644 --- a/docs/source/concat_stokes.rst +++ b/docs/source/concat_stokes.rst @@ -180,7 +180,7 @@ CONCAT_STOKES_IMAGES_AXIS_DEGENERACY See the `source code `__. -This test covers the same 5 combinations as CONCAT_STOKES_IMAGES, but uses axis-degeneracy-dropped ("dropdeg") versions of the Stokes images. This verifies that the backend correctly handles images where degenerate axes have been removed. It checks only the first Stokes plane of each hypercube; it does not walk the Stokes axis the way CONCAT_STOKES_IMAGES does. +This test covers the same 5 combinations as CONCAT_STOKES_IMAGES, in the same 5 steps and with the same per-plane checks, but uses axis-degeneracy-dropped ("dropdeg") versions of the Stokes images. This verifies that the backend correctly handles images where degenerate axes have been removed. **Input files** (from ``set_QA`` directory): @@ -189,43 +189,50 @@ This test covers the same 5 combinations as CONCAT_STOKES_IMAGES, but uses axis- - ``IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits`` (polarizationType = 3) - ``IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits`` (polarizationType = 4) -The same 5 cases are tested with the same expected results: +These four inputs carry **no Stokes axis of their own**, which is the premise of this test: ``DoConcat`` takes its ``stokes_axis < 0`` branch, builds a Stokes coordinate, and extends every image before it can concatenate them. The FILE_INFO_RESPONSE step therefore checks the inputs really are degenerate-axis-dropped, so the test cannot quietly turn into a copy of CONCAT_STOKES_IMAGES: + + - dimensions = 3, width = 256, height = 256, depth = 480, stokes = 1 + +Dropping a degenerate axis changes the shape of the file, not its pixels, so the four reference histograms are exactly those tabulated for CONCAT_STOKES_IMAGES above, and the planes of each hypercube are identified against them the same way. + +The same 5 cases are tested, each requested in reverse order and expected back sorted: .. list-table:: Expected results per combination :header-rows: 1 - :widths: 20 40 10 10 + :widths: 16 34 16 10 10 * - Case - Output file name + - Stokes axis - Stokes - Beam table length * - I,Q,U,V - hypercube_IQUV.dropdeg.manual.pbcor.fits + - I, Q, U, V - 4 - 1920 * - I,V - hypercube_IV.dropdeg.manual.pbcor.fits + - I, V - 2 - 960 * - Q,U - hypercube_QU.dropdeg.manual.pbcor.fits + - Q, U - 2 - 960 * - I,Q,U - hypercube_IQU.dropdeg.manual.pbcor.fits + - I, Q, U - 3 - 1440 * - Q,U,V - hypercube_QUV.dropdeg.manual.pbcor.fits + - Q, U, V - 3 - 1440 -All output files have dimensions = 4, width = 256, height = 256, depth = 480. - -The REGION_HISTOGRAM_DATA values are identical to CONCAT_STOKES_IMAGES: - -- Stokes I first component (Cases 1, 2, 4): binWidth = 0.004779, mean = 0.001407, stdDev = 0.053684 -- Stokes Q first component (Cases 3, 5): binWidth = 0.000163, mean = -0.000037, stdDev = 0.003869 +All output files have dimensions = 4, width = 256, height = 256, depth = 480 — the Stokes axis being the one the concatenation added. CONCAT_ERROR_MESSAGE ~~~~~~~~~~~~~~~~~~~~ diff --git a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts index 5f948259..1e998853 100644 --- a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts @@ -1,6 +1,6 @@ import { CARTA } from 'carta-protobuf'; import config from './config.json'; -import { checkConnection } from './MyClient'; +import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; let testServerUrl: string = config.serverURL0; @@ -8,322 +8,159 @@ let testSubdirectory: string = config.path.QA; let connectTimeout: number = config.timeout.connection; let openFileTimeout = config.timeout.openFile; let concatStokeTimeout = config.timeout.concatStokes; +let changeChannelTimeout = config.timeout.changeChannel; + +type StokesLetter = 'I' | 'Q' | 'U' | 'V'; + +// One "dropdeg" Stokes cube, and the REGION_HISTOGRAM_DATA the backend answers with when that cube +// is opened on its own. The four histograms are what tells the planes of a hypercube apart, so the +// test can say which image ended up on which Stokes plane rather than only counting the planes. +// They are the same values as in CONCAT_STOKES_IMAGES: dropping the degenerate axis changes the +// shape of the file, not its pixels. +interface StokesImage { + file: string; + polarizationType: CARTA.PolarizationType; + histogram: CARTA.IHistogram; +} -interface ConcatStokesFilesAckExt extends CARTA.IConcatStokesFilesAck { - OpenFileAckBeamLength: number; +interface ConcatCase { + title: string; + // StokesFilesConnector iterates its loaders in polarization order, so the Stokes axis it builds + // is sorted no matter how the request was ordered. Every case is requested out of order to hold + // the backend to that. + requestOrder: StokesLetter[]; + expectedPlanes: StokesLetter[]; + hypercubeName: string; } interface AssertItem { - filelist: CARTA.IFileListRequest; - fileInfoReq: CARTA.IFileInfoRequest[]; - ConcatReq: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseIQUV: CARTA.IRegionHistogramData; - ConcatReqIV: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseIV: CARTA.IRegionHistogramData; - ConcatReqQU: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseQU: CARTA.IRegionHistogramData; - ConcatReqIQU: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseIQU: CARTA.IRegionHistogramData; - ConcatReqQUV: CARTA.IConcatStokesFiles; - RegionHistogramDataResponseQUV: CARTA.IRegionHistogramData; - ConcatResponse: ConcatStokesFilesAckExt[]; + fileList: CARTA.IFileListRequest; + stokesImages: Record; + concatCases: ConcatCase[]; + fileId: number; + renderMode: CARTA.RenderMode; + // The inputs carry no Stokes axis of their own, which is the premise of this test: DoConcat has + // to build a Stokes coordinate and extend every image before it can concatenate them. + inputShape: { dimensions: number; width: number; height: number; depth: number; stokes: number }; + hypercubeShape: { dimensions: number; width: number; height: number; depth: number }; + regionHistogram: { regionId: number; progress: number; numBins: number; histogramNumBins: number }; + requiredTiles: CARTA.IAddRequiredTiles; + precisionDigits: number; } let assertItem: AssertItem = { - filelist: { directory: testSubdirectory }, - fileInfoReq: [ - { + fileList: { directory: testSubdirectory }, + stokesImages: { + I: { file: 'IRCp10216_sci.spw0.cube.I.dropdeg.manual.pbcor.fits', - hdu: '', - }, - { - file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', - hdu: '', - }, - { - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - hdu: '', - }, - { - file: 'IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits', - hdu: '', - }, - ], - ConcatReq: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits', - polarizationType: 4, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - polarizationType: 3, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', - polarizationType: 2, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.I.dropdeg.manual.pbcor.fits', - polarizationType: 1, + polarizationType: CARTA.PolarizationType.I, + histogram: { + binWidth: 0.004779201466590166, + firstBinCenter: -0.11032065749168396, + mean: 0.0014072911570091893, + stdDev: 0.05368401551544901, }, - ], - }, - RegionHistogramDataResponseIQUV: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.004779201466590166, - firstBinCenter: -0.11032065749168396, - numBins: 256, - stdDev: 0.05368401551544911, - mean: 0.0014072911570091893, - }, - }, - ConcatReqIV: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits', - polarizationType: 4, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.I.dropdeg.manual.pbcor.fits', - polarizationType: 1, - }, - ], - }, - RegionHistogramDataResponseIV: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.004779201466590166, - firstBinCenter: -0.11032065749168396, - numBins: 256, - stdDev: 0.05368401551544911, - mean: 0.0014072911570091893, }, - }, - ConcatReqQU: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - polarizationType: 3, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', - polarizationType: 2, + Q: { + file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.Q, + histogram: { + binWidth: 0.00016267175669781864, + firstBinCenter: -0.018377140164375305, + mean: -0.00003742659352908538, + stdDev: 0.003869341538017443, }, - ], - }, - RegionHistogramDataResponseQU: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.00016267175669781864, - firstBinCenter: -0.018377140164375305, - numBins: 256, - stdDev: 0.0038693415380174558, - mean: -0.00003742659352908538, }, - }, - ConcatReqIQU: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - polarizationType: 3, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', - polarizationType: 2, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.I.dropdeg.manual.pbcor.fits', - polarizationType: 1, + U: { + file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.U, + histogram: { + binWidth: 0.00016493673319928348, + firstBinCenter: -0.02082323282957077, + mean: 0.00012091044507226787, + stdDev: 0.004009951489450122, }, - ], - }, - RegionHistogramDataResponseIQU: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.004779201466590166, - firstBinCenter: -0.11032065749168396, - numBins: 256, - stdDev: 0.05368401551544911, - mean: 0.0014072911570091893, }, - }, - ConcatReqQUV: { - fileId: 0, - renderMode: 0, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits', - polarizationType: 4, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - polarizationType: 3, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', - polarizationType: 2, + V: { + file: 'IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits', + polarizationType: CARTA.PolarizationType.V, + histogram: { + binWidth: 0.00016941891226451844, + firstBinCenter: -0.020163865759968758, + mean: 0.000017799031213005305, + stdDev: 0.003931388177191896, }, - ], - }, - RegionHistogramDataResponseQUV: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.00016267175669781864, - firstBinCenter: -0.018377140164375305, - numBins: 256, - stdDev: 0.0038693415380174558, - mean: -0.00003742659352908538, }, }, - ConcatResponse: [ + concatCases: [ { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_IQUV.dropdeg.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 4, - width: 256, - }, - }, - OpenFileAckBeamLength: 1920, + title: 'Case 1: Combine I, Q, U & V', + requestOrder: ['V', 'U', 'Q', 'I'], + expectedPlanes: ['I', 'Q', 'U', 'V'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_IQUV.dropdeg.manual.pbcor.fits', }, { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_IV.dropdeg.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 2, - width: 256, - }, - }, - OpenFileAckBeamLength: 960, + title: 'Case 2: Combine I & V', + requestOrder: ['V', 'I'], + expectedPlanes: ['I', 'V'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_IV.dropdeg.manual.pbcor.fits', }, { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_QU.dropdeg.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 2, - width: 256, - }, - }, - OpenFileAckBeamLength: 960, + title: 'Case 3: Combine Q & U', + requestOrder: ['U', 'Q'], + expectedPlanes: ['Q', 'U'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_QU.dropdeg.manual.pbcor.fits', }, { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_IQU.dropdeg.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 3, - width: 256, - }, - }, - OpenFileAckBeamLength: 1440, + title: 'Case 4: Combine I, Q & U', + requestOrder: ['U', 'Q', 'I'], + expectedPlanes: ['I', 'Q', 'U'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_IQU.dropdeg.manual.pbcor.fits', }, { - success: true, - openFileAck: { - success: true, - fileInfo: { - name: 'IRCp10216_sci.spw0.cube.hypercube_QUV.dropdeg.manual.pbcor.fits', - }, - fileInfoExtended: { - depth: 480, - dimensions: 4, - height: 256, - stokes: 3, - width: 256, - }, - }, - OpenFileAckBeamLength: 1440, + title: 'Case 5: Combine Q, U & V', + requestOrder: ['V', 'U', 'Q'], + expectedPlanes: ['Q', 'U', 'V'], + hypercubeName: 'IRCp10216_sci.spw0.cube.hypercube_QUV.dropdeg.manual.pbcor.fits', }, ], + fileId: 0, + renderMode: CARTA.RenderMode.RASTER, + inputShape: { dimensions: 3, width: 256, height: 256, depth: 480, stokes: 1 }, + hypercubeShape: { dimensions: 4, width: 256, height: 256, depth: 480 }, + regionHistogram: { regionId: -1, progress: 1, numBins: -1, histogramNumBins: 256 }, + requiredTiles: { + fileId: 0, + tiles: [0], + compressionType: CARTA.CompressionType.ZFP, + compressionQuality: 11, + }, + precisionDigits: 6, }; let basepath: string; +// The whole point of a hypercube is that the plane the viewer asks for is the image which went into +// it, so each plane is compared against the histogram of the single-Stokes cube it came from. +function assertHistogramMatchesStokesImage( + regionHistogramData: CARTA.IRegionHistogramData, + stokesLetter: StokesLetter +) { + const expected = assertItem.stokesImages[stokesLetter].histogram; + expect(regionHistogramData.fileId).toEqual(assertItem.fileId); + expect(regionHistogramData.regionId).toEqual(assertItem.regionHistogram.regionId); + expect(regionHistogramData.progress).toEqual(assertItem.regionHistogram.progress); + expect(regionHistogramData.config!.numBins).toEqual(assertItem.regionHistogram.numBins); + expect(regionHistogramData.histograms!.numBins).toEqual(assertItem.regionHistogram.histogramNumBins); + expect(regionHistogramData.histograms!.binWidth).toBeCloseTo(expected.binWidth!, assertItem.precisionDigits); + expect(regionHistogramData.histograms!.firstBinCenter).toBeCloseTo( + expected.firstBinCenter!, + assertItem.precisionDigits + ); + expect(regionHistogramData.histograms!.mean).toBeCloseTo(expected.mean!, assertItem.precisionDigits); + expect(regionHistogramData.histograms!.stdDev).toBeCloseTo(expected.stdDev!, assertItem.precisionDigits); +} + describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis-degeneracy stokes images into a single image.', () => { const msgController = MessageController.Instance; beforeAll(async () => { @@ -335,514 +172,115 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- test(`Get the base path and prefix the image directory with it |`, async () => { const fileListResponse = await msgController.getFileList('$BASE', 0); basepath = fileListResponse.directory; - assertItem.filelist.directory = basepath + '/' + assertItem.filelist.directory; - }); - - describe(`Case 1: Combine I,Q,U,V |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); - - assertItem.fileInfoReq.map((input, index) => { - test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, - async () => { - let FileInfoResponse = await msgController.getFileInfo( - assertItem.filelist.directory, - input.file, - input.hdu - ); - expect(FileInfoResponse.success).toEqual(true); - }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReq.stokesFiles.map((input, index) => { - assertItem.ConcatReq.stokesFiles[index].directory = assertItem.filelist.directory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReq.stokesFiles, - assertItem.ConcatReq.fileId, - assertItem.ConcatReq.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseIQUV.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseIQUV.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseIQUV.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQUV.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQUV.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQUV.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseIQUV.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQUV.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[0].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[0].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[0].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[0].openFileAck.fileInfoExtended.depth - ); - }); - }); - - describe(`Case 2: Combine I & V |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); - - let FileInfoResponse: any = []; - let inputIndex = [0, 3]; - inputIndex.map((input, index) => { - test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, - async () => { - FileInfoResponse = await msgController.getFileInfo( - assertItem.filelist.directory, - assertItem.fileInfoReq[input].file, - assertItem.fileInfoReq[input].hdu - ); - expect(FileInfoResponse.success).toEqual(true); - }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqIV.stokesFiles.map((input, index) => { - assertItem.ConcatReqIV.stokesFiles[index].directory = assertItem.filelist.directory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqIV.stokesFiles, - assertItem.ConcatReqIV.fileId, - assertItem.ConcatReqIV.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseIV.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseIV.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseIV.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseIV.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseIV.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseIV.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseIV.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseIV.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[1].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[1].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[1].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[1].openFileAck.fileInfoExtended.depth - ); - }); + assertItem.fileList.directory = basepath + '/' + assertItem.fileList.directory; }); - describe(`Case 3: Combine Q & U |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); + assertItem.concatCases.forEach((concatCase) => { + describe(`${concatCase.title} |`, () => { + let concatStokesResponse: CARTA.IConcatStokesFilesAck; + + test(`(Step 1) FILE_LIST_RESPONSE should list the input images |`, async () => { + const fileListResponse = await msgController.getFileList(assertItem.fileList.directory!, 0); + expect(fileListResponse.success).toEqual(true); + const fileNames = fileListResponse.files!.map((file) => file.name); + concatCase.requestOrder.forEach((stokesLetter) => + expect(fileNames).toContain(assertItem.stokesImages[stokesLetter].file) + ); + }); + + concatCase.requestOrder.forEach((stokesLetter, index) => { + test( + `(Step 2-${index + 1}) FILE_INFO_RESPONSE for the Stokes ${stokesLetter} cube should show a dropped Stokes axis within ${openFileTimeout} ms | `, + async () => { + const stokesImage = assertItem.stokesImages[stokesLetter]; + const fileInfoResponse = await msgController.getFileInfo( + assertItem.fileList.directory!, + stokesImage.file, + '' + ); + expect(fileInfoResponse.success).toEqual(true); + expect(fileInfoResponse.fileInfo!.name).toEqual(stokesImage.file); + // The inputs have to really be degenerate-axis-dropped, otherwise this test + // would quietly become a copy of CONCAT_STOKES_IMAGES. + const fileInfoExtended = fileInfoResponse.fileInfoExtended!['0']; + expect(fileInfoExtended.dimensions).toEqual(assertItem.inputShape.dimensions); + expect(fileInfoExtended.width).toEqual(assertItem.inputShape.width); + expect(fileInfoExtended.height).toEqual(assertItem.inputShape.height); + expect(fileInfoExtended.depth).toEqual(assertItem.inputShape.depth); + expect(fileInfoExtended.stokes).toEqual(assertItem.inputShape.stokes); + }, + openFileTimeout + ); + }); - let FileInfoResponse: any = []; - let inputIndex = [1, 2]; - inputIndex.map((input, index) => { test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, + `(Step 3) CONCAT_STOKES_FILES_ACK and REGION_HISTOGRAM_DATA should arrive within ${concatStokeTimeout} ms | `, async () => { - FileInfoResponse = await msgController.getFileInfo( - assertItem.filelist.directory, - assertItem.fileInfoReq[input].file, - assertItem.fileInfoReq[input].hdu + msgController.closeFile(-1); + const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); + concatStokesResponse = await msgController.loadStokeFiles( + concatCase.requestOrder.map((stokesLetter) => ({ + directory: assertItem.fileList.directory, + hdu: '', + file: assertItem.stokesImages[stokesLetter].file, + polarizationType: assertItem.stokesImages[stokesLetter].polarizationType, + })), + assertItem.fileId, + assertItem.renderMode ); - expect(FileInfoResponse.success).toEqual(true); + const regionHistogramData = await regionHistogramDataStream; + // The hypercube opens on its first plane, which is the lowest polarization of + // the set rather than the one which happened to be requested first. + expect(regionHistogramData[0].stokes).toEqual(0); + assertHistogramMatchesStokesImage(regionHistogramData[0], concatCase.expectedPlanes[0]); }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqQU.stokesFiles.map((input, index) => { - assertItem.ConcatReqQU.stokesFiles[index].directory = assertItem.filelist.directory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqQU.stokesFiles, - assertItem.ConcatReqQU.fileId, - assertItem.ConcatReqQU.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseQU.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseQU.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseQU.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseQU.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseQU.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseQU.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseQU.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseQU.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[2].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[2].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[2].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[2].openFileAck.fileInfoExtended.depth - ); - }); - }); - - describe(`Case 4: Combine I, Q & U |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); - - let FileInfoResponse: any = []; - let inputIndex = [0, 1, 2]; - inputIndex.map((input, index) => { - test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, - async () => { - FileInfoResponse = await msgController.getFileInfo( - assertItem.filelist.directory, - assertItem.fileInfoReq[input].file, - assertItem.fileInfoReq[input].hdu - ); - expect(FileInfoResponse.success).toEqual(true); - }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqIQU.stokesFiles.map((input, index) => { - assertItem.ConcatReqIQU.stokesFiles[index].directory = assertItem.filelist.directory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqIQU.stokesFiles, - assertItem.ConcatReqIQU.fileId, - assertItem.ConcatReqIQU.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseIQU.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseIQU.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseIQU.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQU.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQU.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQU.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseIQU.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseIQU.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[3].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[3].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[3].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[3].openFileAck.fileInfoExtended.depth - ); - }); - }); - - describe(`Case 5: Combine Q, U & V |`, () => { - test(`(Step 1) Assert FileListRequest |`, async () => { - let FileListResponse = await msgController.getFileList(assertItem.filelist.directory, 0); - expect(FileListResponse.success).toEqual(true); - }); - - let FileInfoResponse: any = []; - let inputIndex = [1, 2, 3]; - inputIndex.map((input, index) => { - test( - `FILE_INFO_RESPONSE-${index + 1} should arrive within ${openFileTimeout} ms" | `, - async () => { - FileInfoResponse = await msgController.getFileInfo( - assertItem.filelist.directory, - assertItem.fileInfoReq[input].file, - assertItem.fileInfoReq[input].hdu - ); - expect(FileInfoResponse.success).toEqual(true); - }, - openFileTimeout - ); - }); - - let ConcatStokesResponse: any = []; - test( - `(Step 2) Modify assert concatenate directory and request CONCAT_STOKES_FILES_ACK within ${concatStokeTimeout} ms | `, - async () => { - assertItem.ConcatReqQUV.stokesFiles.map((input, index) => { - assertItem.ConcatReqQUV.stokesFiles[index].directory = assertItem.filelist.directory; - }); - msgController.closeFile(-1); - let regionHistogramDataArray = []; - let regionHistogramDataPromise = new Promise((resolve) => { - msgController.histogramStream.subscribe({ - next: (data) => { - regionHistogramDataArray.push(data); - resolve(regionHistogramDataArray); - }, - }); - }); - ConcatStokesResponse = await msgController.loadStokeFiles( - assertItem.ConcatReqQUV.stokesFiles, - assertItem.ConcatReqQUV.fileId, - assertItem.ConcatReqQUV.renderMode - ); - let RegionHistogramData = await regionHistogramDataPromise; - - expect(RegionHistogramData[0].regionId).toEqual(assertItem.RegionHistogramDataResponseQUV.regionId); - expect(RegionHistogramData[0].progress).toEqual(assertItem.RegionHistogramDataResponseQUV.progress); - expect(RegionHistogramData[0].config.numBins).toEqual( - assertItem.RegionHistogramDataResponseQUV.config.numBins - ); - expect(RegionHistogramData[0].histograms.binWidth).toBeCloseTo( - assertItem.RegionHistogramDataResponseQUV.histograms.binWidth, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.firstBinCenter).toBeCloseTo( - assertItem.RegionHistogramDataResponseQUV.histograms.firstBinCenter, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.mean).toBeCloseTo( - assertItem.RegionHistogramDataResponseQUV.histograms.mean, - assertItem.precisionDigits - ); - expect(RegionHistogramData[0].histograms.numBins).toEqual( - assertItem.RegionHistogramDataResponseQUV.histograms.numBins - ); - expect(RegionHistogramData[0].histograms.stdDev).toBeCloseTo( - assertItem.RegionHistogramDataResponseQUV.histograms.stdDev, - assertItem.precisionDigits - ); - }, - concatStokeTimeout - ); - - test(`(Step 3) Check CONCAT_STOKES_FILES_ACK response | `, () => { - expect(ConcatStokesResponse.success).toEqual(assertItem.ConcatResponse[4].success); - expect(ConcatStokesResponse.openFileAck.success).toEqual(assertItem.ConcatResponse[4].openFileAck.success); - expect(ConcatStokesResponse.openFileAck.beamTable.length).toEqual( - assertItem.ConcatResponse[4].OpenFileAckBeamLength - ); - expect(ConcatStokesResponse.openFileAck.fileInfo.name).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfo.name - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.dimensions).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.dimensions - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.stokes).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.stokes - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.width).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.width - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.height).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.height - ); - expect(ConcatStokesResponse.openFileAck.fileInfoExtended.depth).toEqual( - assertItem.ConcatResponse[4].openFileAck.fileInfoExtended.depth - ); + concatStokeTimeout + ); + + test(`(Step 4) CONCAT_STOKES_FILES_ACK should describe the hypercube | `, () => { + const openFileAck = concatStokesResponse.openFileAck!; + const fileInfoExtended = openFileAck.fileInfoExtended!; + expect(concatStokesResponse.success).toEqual(true); + expect(openFileAck.success).toEqual(true); + expect(openFileAck.fileId).toEqual(assertItem.fileId); + expect(openFileAck.fileInfo!.name).toEqual(concatCase.hypercubeName); + // The inputs were three-dimensional; the Stokes axis below is one DoConcat added. + expect(fileInfoExtended.dimensions).toEqual(assertItem.hypercubeShape.dimensions); + expect(fileInfoExtended.width).toEqual(assertItem.hypercubeShape.width); + expect(fileInfoExtended.height).toEqual(assertItem.hypercubeShape.height); + expect(fileInfoExtended.depth).toEqual(assertItem.hypercubeShape.depth); + expect(fileInfoExtended.stokes).toEqual(concatCase.expectedPlanes.length); + // StokesFilesConnector::DoConcat rebuilds the beam table as one beam per channel per + // Stokes plane, so its length is the shape of the hypercube restated. + expect(openFileAck.beamTable!.length).toEqual( + assertItem.hypercubeShape.depth * concatCase.expectedPlanes.length + ); + }); + + // Plane 0 was covered by the histogram which came with the ack. The remaining planes are + // reached with SET_IMAGE_CHANNELS, which is what the Stokes selector in the frontend + // sends. A concatenation which put the images on the wrong planes, or which silently + // repeated one of them, fails here and nowhere else. + concatCase.expectedPlanes.slice(1).forEach((stokesLetter, index) => { + const stokes = index + 1; + test( + `(Step 5-${stokes}) Stokes plane ${stokes} should hold the ${stokesLetter} image within ${changeChannelTimeout} ms | `, + async () => { + const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); + msgController.setChannels({ + fileId: assertItem.fileId, + channel: 0, + stokes: stokes, + requiredTiles: assertItem.requiredTiles, + }); + const regionHistogramData = await regionHistogramDataStream; + expect(regionHistogramData[0].stokes).toEqual(stokes); + expect(regionHistogramData[0].channel).toEqual(0); + assertHistogramMatchesStokesImage(regionHistogramData[0], stokesLetter); + }, + changeChannelTimeout + ); + }); }); }); From 8064fc901efb29d85808c5197f5509a0904e72c1 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 9 Sep 2026 12:38:11 +0800 Subject: [PATCH 5/7] Share the Stokes histograms and checks between the concatenation tests --- src/test/CONCAT_STOKES_IMAGES.test.ts | 167 ++++------------ ...NCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts | 174 ++++------------- src/test/ConcatStokesHelpers.ts | 183 ++++++++++++++++++ 3 files changed, 254 insertions(+), 270 deletions(-) create mode 100644 src/test/ConcatStokesHelpers.ts diff --git a/src/test/CONCAT_STOKES_IMAGES.test.ts b/src/test/CONCAT_STOKES_IMAGES.test.ts index 85f57e14..00ab897e 100644 --- a/src/test/CONCAT_STOKES_IMAGES.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES.test.ts @@ -2,6 +2,17 @@ import { CARTA } from 'carta-protobuf'; import config from './config.json'; import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; +import { + ConcatCase, + ImageShape, + StokesImage, + StokesLetter, + assertHistogramMatchesStokesImage, + assertHypercubeAck, + assertStokesPlane, + stokesFile, + stokesImages, +} from './ConcatStokesHelpers'; let testServerUrl: string = config.serverURL0; let testSubdirectory: string = config.path.QA; @@ -10,83 +21,23 @@ let openFileTimeout = config.timeout.openFile; let concatStokeTimeout = config.timeout.concatStokes; let changeChannelTimeout = config.timeout.changeChannel; -type StokesLetter = 'I' | 'Q' | 'U' | 'V'; - -// One Stokes cube, and the REGION_HISTOGRAM_DATA the backend answers with when that cube is opened -// on its own. The four histograms are what tells the planes of a hypercube apart, so the test can -// say which image ended up on which Stokes plane rather than only counting the planes. -interface StokesImage { - file: string; - polarizationType: CARTA.PolarizationType; - histogram: CARTA.IHistogram; -} - -interface ConcatCase { - title: string; - // StokesFilesConnector iterates its loaders in polarization order, so the Stokes axis it builds - // is sorted no matter how the request was ordered. Every case is requested out of order to hold - // the backend to that. - requestOrder: StokesLetter[]; - expectedPlanes: StokesLetter[]; - hypercubeName: string; -} - interface AssertItem { fileList: CARTA.IFileListRequest; stokesImages: Record; concatCases: ConcatCase[]; fileId: number; renderMode: CARTA.RenderMode; - imageShape: { dimensions: number; width: number; height: number; depth: number }; - regionHistogram: { regionId: number; progress: number; numBins: number; histogramNumBins: number }; - requiredTiles: CARTA.IAddRequiredTiles; - precisionDigits: number; + imageShape: ImageShape; } let assertItem: AssertItem = { fileList: { directory: testSubdirectory }, - stokesImages: { - I: { - file: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.I, - histogram: { - binWidth: 0.004779201466590166, - firstBinCenter: -0.11032065749168396, - mean: 0.0014072911570091893, - stdDev: 0.05368401551544901, - }, - }, - Q: { - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - histogram: { - binWidth: 0.00016267175669781864, - firstBinCenter: -0.018377140164375305, - mean: -0.00003742659352908538, - stdDev: 0.003869341538017443, - }, - }, - U: { - file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.U, - histogram: { - binWidth: 0.00016493673319928348, - firstBinCenter: -0.02082323282957077, - mean: 0.00012091044507226787, - stdDev: 0.004009951489450122, - }, - }, - V: { - file: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.V, - histogram: { - binWidth: 0.00016941891226451844, - firstBinCenter: -0.020163865759968758, - mean: 0.000017799031213005305, - stdDev: 0.003931388177191896, - }, - }, - }, + stokesImages: stokesImages({ + I: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', + Q: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + U: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', + V: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', + }), concatCases: [ { title: 'Case 1: Combine I, Q, U & V', @@ -123,39 +74,10 @@ let assertItem: AssertItem = { renderMode: CARTA.RenderMode.RASTER, // Every input cube has this shape, and concatenation only adds the Stokes axis to it. imageShape: { dimensions: 4, width: 256, height: 256, depth: 480 }, - regionHistogram: { regionId: -1, progress: 1, numBins: -1, histogramNumBins: 256 }, - requiredTiles: { - fileId: 0, - tiles: [0], - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - }, - precisionDigits: 6, }; let basepath: string; -// The whole point of a hypercube is that the plane the viewer asks for is the image which went into -// it, so each plane is compared against the histogram of the single-Stokes cube it came from. -function assertHistogramMatchesStokesImage( - regionHistogramData: CARTA.IRegionHistogramData, - stokesLetter: StokesLetter -) { - const expected = assertItem.stokesImages[stokesLetter].histogram; - expect(regionHistogramData.fileId).toEqual(assertItem.fileId); - expect(regionHistogramData.regionId).toEqual(assertItem.regionHistogram.regionId); - expect(regionHistogramData.progress).toEqual(assertItem.regionHistogram.progress); - expect(regionHistogramData.config!.numBins).toEqual(assertItem.regionHistogram.numBins); - expect(regionHistogramData.histograms!.numBins).toEqual(assertItem.regionHistogram.histogramNumBins); - expect(regionHistogramData.histograms!.binWidth).toBeCloseTo(expected.binWidth!, assertItem.precisionDigits); - expect(regionHistogramData.histograms!.firstBinCenter).toBeCloseTo( - expected.firstBinCenter!, - assertItem.precisionDigits - ); - expect(regionHistogramData.histograms!.mean).toBeCloseTo(expected.mean!, assertItem.precisionDigits); - expect(regionHistogramData.histograms!.stdDev).toBeCloseTo(expected.stdDev!, assertItem.precisionDigits); -} - describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into single image', () => { const msgController = MessageController.Instance; beforeAll(async () => { @@ -206,12 +128,9 @@ describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into si msgController.closeFile(-1); const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); concatStokesResponse = await msgController.loadStokeFiles( - concatCase.requestOrder.map((stokesLetter) => ({ - directory: assertItem.fileList.directory, - hdu: '', - file: assertItem.stokesImages[stokesLetter].file, - polarizationType: assertItem.stokesImages[stokesLetter].polarizationType, - })), + concatCase.requestOrder.map((stokesLetter) => + stokesFile(assertItem.stokesImages[stokesLetter], assertItem.fileList.directory!) + ), assertItem.fileId, assertItem.renderMode ); @@ -219,50 +138,32 @@ describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into si // The hypercube opens on its first plane, which is the lowest polarization of // the set rather than the one which happened to be requested first. expect(regionHistogramData[0].stokes).toEqual(0); - assertHistogramMatchesStokesImage(regionHistogramData[0], concatCase.expectedPlanes[0]); + assertHistogramMatchesStokesImage( + regionHistogramData[0], + concatCase.expectedPlanes[0], + assertItem.fileId + ); }, concatStokeTimeout ); test(`(Step 4) CONCAT_STOKES_FILES_ACK should describe the hypercube | `, () => { - const openFileAck = concatStokesResponse.openFileAck!; - const fileInfoExtended = openFileAck.fileInfoExtended!; - expect(concatStokesResponse.success).toEqual(true); - expect(openFileAck.success).toEqual(true); - expect(openFileAck.fileId).toEqual(assertItem.fileId); - expect(openFileAck.fileInfo!.name).toEqual(concatCase.hypercubeName); - expect(fileInfoExtended.dimensions).toEqual(assertItem.imageShape.dimensions); - expect(fileInfoExtended.width).toEqual(assertItem.imageShape.width); - expect(fileInfoExtended.height).toEqual(assertItem.imageShape.height); - expect(fileInfoExtended.depth).toEqual(assertItem.imageShape.depth); - expect(fileInfoExtended.stokes).toEqual(concatCase.expectedPlanes.length); - // StokesFilesConnector::DoConcat rebuilds the beam table as one beam per channel per - // Stokes plane, so its length is the shape of the hypercube restated. - expect(openFileAck.beamTable!.length).toEqual( - assertItem.imageShape.depth * concatCase.expectedPlanes.length - ); + assertHypercubeAck(concatStokesResponse, { + fileId: assertItem.fileId, + name: concatCase.hypercubeName, + shape: assertItem.imageShape, + planes: concatCase.expectedPlanes, + }); }); // Plane 0 was covered by the histogram which came with the ack. The remaining planes are - // reached with SET_IMAGE_CHANNELS, which is what the Stokes selector in the frontend - // sends. A concatenation which put the images on the wrong planes, or which silently - // repeated one of them, fails here and nowhere else. + // reached with SET_IMAGE_CHANNELS. concatCase.expectedPlanes.slice(1).forEach((stokesLetter, index) => { const stokes = index + 1; test( `(Step 5-${stokes}) Stokes plane ${stokes} should hold the ${stokesLetter} image within ${changeChannelTimeout} ms | `, async () => { - const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); - msgController.setChannels({ - fileId: assertItem.fileId, - channel: 0, - stokes: stokes, - requiredTiles: assertItem.requiredTiles, - }); - const regionHistogramData = await regionHistogramDataStream; - expect(regionHistogramData[0].stokes).toEqual(stokes); - expect(regionHistogramData[0].channel).toEqual(0); - assertHistogramMatchesStokesImage(regionHistogramData[0], stokesLetter); + await assertStokesPlane(assertItem.fileId, stokes, stokesLetter); }, changeChannelTimeout ); diff --git a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts index 1e998853..40c2c54f 100644 --- a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts @@ -2,6 +2,17 @@ import { CARTA } from 'carta-protobuf'; import config from './config.json'; import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; +import { + ConcatCase, + ImageShape, + StokesImage, + StokesLetter, + assertHistogramMatchesStokesImage, + assertHypercubeAck, + assertStokesPlane, + stokesFile, + stokesImages, +} from './ConcatStokesHelpers'; let testServerUrl: string = config.serverURL0; let testSubdirectory: string = config.path.QA; @@ -10,29 +21,6 @@ let openFileTimeout = config.timeout.openFile; let concatStokeTimeout = config.timeout.concatStokes; let changeChannelTimeout = config.timeout.changeChannel; -type StokesLetter = 'I' | 'Q' | 'U' | 'V'; - -// One "dropdeg" Stokes cube, and the REGION_HISTOGRAM_DATA the backend answers with when that cube -// is opened on its own. The four histograms are what tells the planes of a hypercube apart, so the -// test can say which image ended up on which Stokes plane rather than only counting the planes. -// They are the same values as in CONCAT_STOKES_IMAGES: dropping the degenerate axis changes the -// shape of the file, not its pixels. -interface StokesImage { - file: string; - polarizationType: CARTA.PolarizationType; - histogram: CARTA.IHistogram; -} - -interface ConcatCase { - title: string; - // StokesFilesConnector iterates its loaders in polarization order, so the Stokes axis it builds - // is sorted no matter how the request was ordered. Every case is requested out of order to hold - // the backend to that. - requestOrder: StokesLetter[]; - expectedPlanes: StokesLetter[]; - hypercubeName: string; -} - interface AssertItem { fileList: CARTA.IFileListRequest; stokesImages: Record; @@ -41,57 +29,18 @@ interface AssertItem { renderMode: CARTA.RenderMode; // The inputs carry no Stokes axis of their own, which is the premise of this test: DoConcat has // to build a Stokes coordinate and extend every image before it can concatenate them. - inputShape: { dimensions: number; width: number; height: number; depth: number; stokes: number }; - hypercubeShape: { dimensions: number; width: number; height: number; depth: number }; - regionHistogram: { regionId: number; progress: number; numBins: number; histogramNumBins: number }; - requiredTiles: CARTA.IAddRequiredTiles; - precisionDigits: number; + inputShape: ImageShape & { stokes: number }; + hypercubeShape: ImageShape; } let assertItem: AssertItem = { fileList: { directory: testSubdirectory }, - stokesImages: { - I: { - file: 'IRCp10216_sci.spw0.cube.I.dropdeg.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.I, - histogram: { - binWidth: 0.004779201466590166, - firstBinCenter: -0.11032065749168396, - mean: 0.0014072911570091893, - stdDev: 0.05368401551544901, - }, - }, - Q: { - file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - histogram: { - binWidth: 0.00016267175669781864, - firstBinCenter: -0.018377140164375305, - mean: -0.00003742659352908538, - stdDev: 0.003869341538017443, - }, - }, - U: { - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.U, - histogram: { - binWidth: 0.00016493673319928348, - firstBinCenter: -0.02082323282957077, - mean: 0.00012091044507226787, - stdDev: 0.004009951489450122, - }, - }, - V: { - file: 'IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.V, - histogram: { - binWidth: 0.00016941891226451844, - firstBinCenter: -0.020163865759968758, - mean: 0.000017799031213005305, - stdDev: 0.003931388177191896, - }, - }, - }, + stokesImages: stokesImages({ + I: 'IRCp10216_sci.spw0.cube.I.dropdeg.manual.pbcor.fits', + Q: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', + U: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', + V: 'IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits', + }), concatCases: [ { title: 'Case 1: Combine I, Q, U & V', @@ -128,39 +77,10 @@ let assertItem: AssertItem = { renderMode: CARTA.RenderMode.RASTER, inputShape: { dimensions: 3, width: 256, height: 256, depth: 480, stokes: 1 }, hypercubeShape: { dimensions: 4, width: 256, height: 256, depth: 480 }, - regionHistogram: { regionId: -1, progress: 1, numBins: -1, histogramNumBins: 256 }, - requiredTiles: { - fileId: 0, - tiles: [0], - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - }, - precisionDigits: 6, }; let basepath: string; -// The whole point of a hypercube is that the plane the viewer asks for is the image which went into -// it, so each plane is compared against the histogram of the single-Stokes cube it came from. -function assertHistogramMatchesStokesImage( - regionHistogramData: CARTA.IRegionHistogramData, - stokesLetter: StokesLetter -) { - const expected = assertItem.stokesImages[stokesLetter].histogram; - expect(regionHistogramData.fileId).toEqual(assertItem.fileId); - expect(regionHistogramData.regionId).toEqual(assertItem.regionHistogram.regionId); - expect(regionHistogramData.progress).toEqual(assertItem.regionHistogram.progress); - expect(regionHistogramData.config!.numBins).toEqual(assertItem.regionHistogram.numBins); - expect(regionHistogramData.histograms!.numBins).toEqual(assertItem.regionHistogram.histogramNumBins); - expect(regionHistogramData.histograms!.binWidth).toBeCloseTo(expected.binWidth!, assertItem.precisionDigits); - expect(regionHistogramData.histograms!.firstBinCenter).toBeCloseTo( - expected.firstBinCenter!, - assertItem.precisionDigits - ); - expect(regionHistogramData.histograms!.mean).toBeCloseTo(expected.mean!, assertItem.precisionDigits); - expect(regionHistogramData.histograms!.stdDev).toBeCloseTo(expected.stdDev!, assertItem.precisionDigits); -} - describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis-degeneracy stokes images into a single image.', () => { const msgController = MessageController.Instance; beforeAll(async () => { @@ -219,12 +139,9 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- msgController.closeFile(-1); const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); concatStokesResponse = await msgController.loadStokeFiles( - concatCase.requestOrder.map((stokesLetter) => ({ - directory: assertItem.fileList.directory, - hdu: '', - file: assertItem.stokesImages[stokesLetter].file, - polarizationType: assertItem.stokesImages[stokesLetter].polarizationType, - })), + concatCase.requestOrder.map((stokesLetter) => + stokesFile(assertItem.stokesImages[stokesLetter], assertItem.fileList.directory!) + ), assertItem.fileId, assertItem.renderMode ); @@ -232,51 +149,34 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- // The hypercube opens on its first plane, which is the lowest polarization of // the set rather than the one which happened to be requested first. expect(regionHistogramData[0].stokes).toEqual(0); - assertHistogramMatchesStokesImage(regionHistogramData[0], concatCase.expectedPlanes[0]); + assertHistogramMatchesStokesImage( + regionHistogramData[0], + concatCase.expectedPlanes[0], + assertItem.fileId + ); }, concatStokeTimeout ); + // The inputs were three-dimensional; the Stokes axis of the hypercube is one DoConcat + // added. test(`(Step 4) CONCAT_STOKES_FILES_ACK should describe the hypercube | `, () => { - const openFileAck = concatStokesResponse.openFileAck!; - const fileInfoExtended = openFileAck.fileInfoExtended!; - expect(concatStokesResponse.success).toEqual(true); - expect(openFileAck.success).toEqual(true); - expect(openFileAck.fileId).toEqual(assertItem.fileId); - expect(openFileAck.fileInfo!.name).toEqual(concatCase.hypercubeName); - // The inputs were three-dimensional; the Stokes axis below is one DoConcat added. - expect(fileInfoExtended.dimensions).toEqual(assertItem.hypercubeShape.dimensions); - expect(fileInfoExtended.width).toEqual(assertItem.hypercubeShape.width); - expect(fileInfoExtended.height).toEqual(assertItem.hypercubeShape.height); - expect(fileInfoExtended.depth).toEqual(assertItem.hypercubeShape.depth); - expect(fileInfoExtended.stokes).toEqual(concatCase.expectedPlanes.length); - // StokesFilesConnector::DoConcat rebuilds the beam table as one beam per channel per - // Stokes plane, so its length is the shape of the hypercube restated. - expect(openFileAck.beamTable!.length).toEqual( - assertItem.hypercubeShape.depth * concatCase.expectedPlanes.length - ); + assertHypercubeAck(concatStokesResponse, { + fileId: assertItem.fileId, + name: concatCase.hypercubeName, + shape: assertItem.hypercubeShape, + planes: concatCase.expectedPlanes, + }); }); // Plane 0 was covered by the histogram which came with the ack. The remaining planes are - // reached with SET_IMAGE_CHANNELS, which is what the Stokes selector in the frontend - // sends. A concatenation which put the images on the wrong planes, or which silently - // repeated one of them, fails here and nowhere else. + // reached with SET_IMAGE_CHANNELS. concatCase.expectedPlanes.slice(1).forEach((stokesLetter, index) => { const stokes = index + 1; test( `(Step 5-${stokes}) Stokes plane ${stokes} should hold the ${stokesLetter} image within ${changeChannelTimeout} ms | `, async () => { - const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); - msgController.setChannels({ - fileId: assertItem.fileId, - channel: 0, - stokes: stokes, - requiredTiles: assertItem.requiredTiles, - }); - const regionHistogramData = await regionHistogramDataStream; - expect(regionHistogramData[0].stokes).toEqual(stokes); - expect(regionHistogramData[0].channel).toEqual(0); - assertHistogramMatchesStokesImage(regionHistogramData[0], stokesLetter); + await assertStokesPlane(assertItem.fileId, stokes, stokesLetter); }, changeChannelTimeout ); diff --git a/src/test/ConcatStokesHelpers.ts b/src/test/ConcatStokesHelpers.ts new file mode 100644 index 00000000..c289cd3f --- /dev/null +++ b/src/test/ConcatStokesHelpers.ts @@ -0,0 +1,183 @@ +import { CARTA } from 'carta-protobuf'; +import { Stream } from './MyClient'; +import { MessageController } from './MessageController'; + +/** + * Shared fixtures and assertions for the CONCAT_STOKES_* tests. Both of them concatenate the same + * four single-Stokes cubes into the same five hypercubes, one from cubes which carry a degenerate + * Stokes axis and one from cubes which do not, so everything except the file names and the shape + * of the inputs is the same on both sides. + * + * These are plain assertions rather than jest tests: the test titles and timeouts belong to the + * test files, so that every test( ) a file registers can be read there. + */ + +export type StokesLetter = 'I' | 'Q' | 'U' | 'V'; + +/** + * One Stokes cube, and the REGION_HISTOGRAM_DATA the backend answers with when that cube is opened + * on its own. The four histograms are what tells the planes of a hypercube apart, so a test can say + * which image ended up on which Stokes plane rather than only counting the planes. + */ +export interface StokesImage { + file: string; + polarizationType: CARTA.PolarizationType; + histogram: CARTA.IHistogram; +} + +export interface ConcatCase { + title: string; + /** + * StokesFilesConnector iterates its loaders in polarization order, so the Stokes axis it builds + * is sorted no matter how the request was ordered. Every case is requested out of order to hold + * the backend to that. + */ + requestOrder: StokesLetter[]; + expectedPlanes: StokesLetter[]; + hypercubeName: string; +} + +export interface ImageShape { + dimensions: number; + width: number; + height: number; + depth: number; +} + +export const POLARIZATION_TYPES: Record = { + I: CARTA.PolarizationType.I, + Q: CARTA.PolarizationType.Q, + U: CARTA.PolarizationType.U, + V: CARTA.PolarizationType.V, +}; + +/** + * The REGION_HISTOGRAM_DATA of each single-Stokes cube opened on its own. One copy serves both + * tests: dropping the degenerate axis changes the shape of a file, not its pixels. + */ +export const STOKES_HISTOGRAMS: Record = { + I: { + binWidth: 0.004779201466590166, + firstBinCenter: -0.11032065749168396, + mean: 0.0014072911570091893, + stdDev: 0.05368401551544901, + }, + Q: { + binWidth: 0.00016267175669781864, + firstBinCenter: -0.018377140164375305, + mean: -0.00003742659352908538, + stdDev: 0.003869341538017443, + }, + U: { + binWidth: 0.00016493673319928348, + firstBinCenter: -0.02082323282957077, + mean: 0.00012091044507226787, + stdDev: 0.004009951489450122, + }, + V: { + binWidth: 0.00016941891226451844, + firstBinCenter: -0.020163865759968758, + mean: 0.000017799031213005305, + stdDev: 0.003931388177191896, + }, +}; + +/** The fields of REGION_HISTOGRAM_DATA the backend fills the same way for every plane of every image. */ +const REGION_HISTOGRAM = { regionId: -1, progress: 1, numBins: -1, histogramNumBins: 256 }; + +/** The bin width and the moments are floats the backend computes, so they are compared to this many digits. */ +const PRECISION_DIGITS = 6; + +/** + * The Stokes image table of one variant: the four file names it is built from, with the + * polarization types and the histograms, which do not vary between the variants. + */ +export function stokesImages(files: Record): Record { + const images = {} as Record; + (Object.keys(files) as StokesLetter[]).forEach((stokesLetter) => { + images[stokesLetter] = { + file: files[stokesLetter], + polarizationType: POLARIZATION_TYPES[stokesLetter], + histogram: STOKES_HISTOGRAMS[stokesLetter], + }; + }); + return images; +} + +/** One entry of a CONCAT_STOKES_FILES request. */ +export function stokesFile(image: StokesImage, directory: string): CARTA.IStokesFile { + return { + directory: directory, + hdu: '', + file: image.file, + polarizationType: image.polarizationType, + }; +} + +/** + * The whole point of a hypercube is that the plane the viewer asks for is the image which went into + * it, so each plane is compared against the histogram of the single-Stokes cube it came from. + */ +export function assertHistogramMatchesStokesImage( + regionHistogramData: CARTA.IRegionHistogramData, + stokesLetter: StokesLetter, + fileId: number +) { + const expected = STOKES_HISTOGRAMS[stokesLetter]; + expect(regionHistogramData.fileId).toEqual(fileId); + expect(regionHistogramData.regionId).toEqual(REGION_HISTOGRAM.regionId); + expect(regionHistogramData.progress).toEqual(REGION_HISTOGRAM.progress); + expect(regionHistogramData.config!.numBins).toEqual(REGION_HISTOGRAM.numBins); + expect(regionHistogramData.histograms!.numBins).toEqual(REGION_HISTOGRAM.histogramNumBins); + expect(regionHistogramData.histograms!.binWidth).toBeCloseTo(expected.binWidth!, PRECISION_DIGITS); + expect(regionHistogramData.histograms!.firstBinCenter).toBeCloseTo(expected.firstBinCenter!, PRECISION_DIGITS); + expect(regionHistogramData.histograms!.mean).toBeCloseTo(expected.mean!, PRECISION_DIGITS); + expect(regionHistogramData.histograms!.stdDev).toBeCloseTo(expected.stdDev!, PRECISION_DIGITS); +} + +/** The hypercube the ack describes: its name, its shape, and one Stokes plane per image which went in. */ +export function assertHypercubeAck( + concatStokesResponse: CARTA.IConcatStokesFilesAck, + expected: { fileId: number; name: string; shape: ImageShape; planes: StokesLetter[] } +) { + const openFileAck = concatStokesResponse.openFileAck!; + const fileInfoExtended = openFileAck.fileInfoExtended!; + expect(concatStokesResponse.success).toEqual(true); + expect(openFileAck.success).toEqual(true); + expect(openFileAck.fileId).toEqual(expected.fileId); + expect(openFileAck.fileInfo!.name).toEqual(expected.name); + expect(fileInfoExtended.dimensions).toEqual(expected.shape.dimensions); + expect(fileInfoExtended.width).toEqual(expected.shape.width); + expect(fileInfoExtended.height).toEqual(expected.shape.height); + expect(fileInfoExtended.depth).toEqual(expected.shape.depth); + expect(fileInfoExtended.stokes).toEqual(expected.planes.length); + // StokesFilesConnector::DoConcat rebuilds the beam table as one beam per channel per Stokes + // plane, so its length is the shape of the hypercube restated. + expect(openFileAck.beamTable!.length).toEqual(expected.shape.depth * expected.planes.length); +} + +/** + * Reach one plane of the open hypercube with SET_IMAGE_CHANNELS, which is what the Stokes selector + * in the frontend sends, and check that the image which went in on that plane is the one which + * comes back. A concatenation which put the images on the wrong planes, or which silently repeated + * one of them, fails here and nowhere else. + */ +export async function assertStokesPlane(fileId: number, stokes: number, stokesLetter: StokesLetter) { + const msgController = MessageController.Instance; + const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); + msgController.setChannels({ + fileId: fileId, + channel: 0, + stokes: stokes, + requiredTiles: { + fileId: fileId, + tiles: [0], + compressionType: CARTA.CompressionType.ZFP, + compressionQuality: 11, + }, + }); + const regionHistogramData = await regionHistogramDataStream; + expect(regionHistogramData[0].stokes).toEqual(stokes); + expect(regionHistogramData[0].channel).toEqual(0); + assertHistogramMatchesStokesImage(regionHistogramData[0], stokesLetter, fileId); +} From d15651c6056b6a57184a7ac424fae74787658ffc Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 9 Sep 2026 12:45:07 +0800 Subject: [PATCH 6/7] Build the concatenation error cases from named images --- src/test/CONCAT_ERROR_MESSAGE.test.ts | 134 +++++------------- src/test/CONCAT_STOKES_IMAGES.test.ts | 6 +- ...NCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts | 6 +- src/test/ConcatStokesHelpers.ts | 13 +- 4 files changed, 58 insertions(+), 101 deletions(-) diff --git a/src/test/CONCAT_ERROR_MESSAGE.test.ts b/src/test/CONCAT_ERROR_MESSAGE.test.ts index 69b1ba0b..066fca64 100644 --- a/src/test/CONCAT_ERROR_MESSAGE.test.ts +++ b/src/test/CONCAT_ERROR_MESSAGE.test.ts @@ -2,6 +2,7 @@ import { CARTA } from 'carta-protobuf'; import config from './config.json'; import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; +import { stokesFile } from './ConcatStokesHelpers'; let testServerUrl: string = config.serverURL0; let testSubdirectory: string = config.path.QA; @@ -11,6 +12,24 @@ let concatStokeTimeout = config.timeout.concatStokes; // this is how long the backend is watched for a further one. let quietTime: number = config.timeout.messageEvent; +/** The single-Stokes cubes the valid cases are built from, and which CONCAT_STOKES_IMAGES concatenates. */ +const stokesCube = { + I: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', + Q: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', + U: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', + V: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', +}; + +/** The same cubes with the degenerate Stokes axis dropped, so they disagree in shape with the above. */ +const droppedAxisCube = { + Q: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', + U: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', +}; + +/** An image of another file type, and a name which is on no disk. */ +const casaImage = 'M17_SWex.image'; +const missingImage = 'no_such_stokes_image.fits'; + // Every rejection the backend can answer CONCAT_STOKES_FILES with is raised in // StokesFilesConnector::OpenStokesFiles or StokesFilesConnector::StokesFilesValid and travels back // in the message field of an unsuccessful CONCAT_STOKES_FILES_ACK. @@ -36,129 +55,54 @@ let assertItem: AssertItem = { { title: 'Case 1: Q and axis-degeneracy U, image shapes inconsistent', stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.U, - }, + stokesFile(stokesCube.Q, 'Q', testSubdirectory), + stokesFile(droppedAxisCube.U, 'U', testSubdirectory), ], expectedError: 'Image shapes or axes are not consistent!', }, { title: 'Case 2: Q and axis-degeneracy Q, duplicated Stokes type', stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - }, + stokesFile(stokesCube.Q, 'Q', testSubdirectory), + stokesFile(droppedAxisCube.Q, 'Q', testSubdirectory), ], expectedError: 'Duplicate Stokes type found!', }, { title: 'Case 3: a single file, too few to concatenate', - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - }, - ], + stokesFiles: [stokesFile(stokesCube.Q, 'Q', testSubdirectory)], expectedError: 'Need at least two files to concatenate!', }, { title: 'Case 4: a FITS image and a CASA image, mixed file types', stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'M17_SWex.image', - polarizationType: CARTA.PolarizationType.U, - }, + stokesFile(stokesCube.Q, 'Q', testSubdirectory), + stokesFile(casaImage, 'U', testSubdirectory), ], expectedError: 'Different file types can not be concatenated!', }, { title: 'Case 5: I, Q and V, a hypercube with a gap in the Stokes axis', stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.I, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.V.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.V, - }, + stokesFile(stokesCube.I, 'I', testSubdirectory), + stokesFile(stokesCube.Q, 'Q', testSubdirectory), + stokesFile(stokesCube.V, 'V', testSubdirectory), ], expectedError: 'Hypercube IQV is not allowed!', }, { title: 'Case 6: a file which is not on disk', stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'no_such_stokes_image.fits', - polarizationType: CARTA.PolarizationType.U, - }, + stokesFile(stokesCube.Q, 'Q', testSubdirectory), + stokesFile(missingImage, 'U', testSubdirectory), ], - expectedError: 'no_such_stokes_image.fits does not exist.', + expectedError: `${missingImage} does not exist.`, }, ], validConcat: { fileId: 0, renderMode: CARTA.RenderMode.RASTER, - stokesFiles: [ - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.Q, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.manual.pbcor.fits', - polarizationType: CARTA.PolarizationType.U, - }, - ], + stokesFiles: [stokesFile(stokesCube.Q, 'Q', testSubdirectory), stokesFile(stokesCube.U, 'U', testSubdirectory)], }, validConcatName: 'IRCp10216_sci.spw0.cube.hypercube_QU.manual.pbcor.fits', setCursor: { x: 128, y: 128 }, @@ -215,12 +159,12 @@ describe('CONCAT_ERROR_MESSAGE test: incompatible Stokes images are refused with basepath = fileListResponse.directory; assertItem.fileList.directory = basepath + '/' + assertItem.fileList.directory; assertItem.errorCases.forEach((errorCase) => { - errorCase.stokesFiles.forEach((stokesFile) => { - stokesFile.directory = basepath + '/' + stokesFile.directory; + errorCase.stokesFiles.forEach((request) => { + request.directory = basepath + '/' + request.directory; }); }); - assertItem.validConcat.stokesFiles!.forEach((stokesFile) => { - stokesFile.directory = basepath + '/' + stokesFile.directory; + assertItem.validConcat.stokesFiles!.forEach((request) => { + request.directory = basepath + '/' + request.directory; }); }); @@ -233,8 +177,8 @@ describe('CONCAT_ERROR_MESSAGE test: incompatible Stokes images are refused with assertItem.errorCases .flatMap((errorCase) => errorCase.stokesFiles) .concat(assertItem.validConcat.stokesFiles!) - .map((stokesFile) => stokesFile.file!) - .filter((file) => file !== 'no_such_stokes_image.fits') + .map((request) => request.file!) + .filter((file) => file !== missingImage) .forEach((file) => expect(fileNames).toContain(file)); }); diff --git a/src/test/CONCAT_STOKES_IMAGES.test.ts b/src/test/CONCAT_STOKES_IMAGES.test.ts index 00ab897e..d1b79131 100644 --- a/src/test/CONCAT_STOKES_IMAGES.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES.test.ts @@ -129,7 +129,11 @@ describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into si const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); concatStokesResponse = await msgController.loadStokeFiles( concatCase.requestOrder.map((stokesLetter) => - stokesFile(assertItem.stokesImages[stokesLetter], assertItem.fileList.directory!) + stokesFile( + assertItem.stokesImages[stokesLetter].file, + stokesLetter, + assertItem.fileList.directory! + ) ), assertItem.fileId, assertItem.renderMode diff --git a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts index 40c2c54f..df82134a 100644 --- a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts @@ -140,7 +140,11 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); concatStokesResponse = await msgController.loadStokeFiles( concatCase.requestOrder.map((stokesLetter) => - stokesFile(assertItem.stokesImages[stokesLetter], assertItem.fileList.directory!) + stokesFile( + assertItem.stokesImages[stokesLetter].file, + stokesLetter, + assertItem.fileList.directory! + ) ), assertItem.fileId, assertItem.renderMode diff --git a/src/test/ConcatStokesHelpers.ts b/src/test/ConcatStokesHelpers.ts index c289cd3f..76fc763d 100644 --- a/src/test/ConcatStokesHelpers.ts +++ b/src/test/ConcatStokesHelpers.ts @@ -104,13 +104,18 @@ export function stokesImages(files: Record): Record Date: Wed, 9 Sep 2026 13:19:28 +0800 Subject: [PATCH 7/7] Take the settings and the base path step from the common helpers --- src/test/CONCAT_ERROR_MESSAGE.test.ts | 83 +++++++++---------- src/test/CONCAT_STOKES_IMAGES.test.ts | 41 +++++---- ...NCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts | 41 +++++---- src/test/CommonHelpers.ts | 2 + 4 files changed, 79 insertions(+), 88 deletions(-) diff --git a/src/test/CONCAT_ERROR_MESSAGE.test.ts b/src/test/CONCAT_ERROR_MESSAGE.test.ts index 066fca64..08313fa7 100644 --- a/src/test/CONCAT_ERROR_MESSAGE.test.ts +++ b/src/test/CONCAT_ERROR_MESSAGE.test.ts @@ -1,16 +1,15 @@ import { CARTA } from 'carta-protobuf'; -import config from './config.json'; import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; import { stokesFile } from './ConcatStokesHelpers'; - -let testServerUrl: string = config.serverURL0; -let testSubdirectory: string = config.path.QA; -let connectTimeout: number = config.timeout.connection; -let concatStokeTimeout = config.timeout.concatStokes; -// CONCAT_STOKES_FILES_ACK is the only message a rejected concatenation is allowed to draw, and -// this is how long the backend is watched for a further one. -let quietTime: number = config.timeout.messageEvent; +import { + CONCAT_STOKES_TIMEOUT, + CONNECTION_TIMEOUT, + QUIET_TIME, + TEST_SERVER_URL, + TEST_SUBDIRECTORY, + assertBasePath, +} from './CommonHelpers'; /** The single-Stokes cubes the valid cases are built from, and which CONCAT_STOKES_IMAGES concatenates. */ const stokesCube = { @@ -50,51 +49,51 @@ interface AssertItem { } let assertItem: AssertItem = { - fileList: { directory: testSubdirectory }, + fileList: { directory: TEST_SUBDIRECTORY }, errorCases: [ { title: 'Case 1: Q and axis-degeneracy U, image shapes inconsistent', stokesFiles: [ - stokesFile(stokesCube.Q, 'Q', testSubdirectory), - stokesFile(droppedAxisCube.U, 'U', testSubdirectory), + stokesFile(stokesCube.Q, 'Q', TEST_SUBDIRECTORY), + stokesFile(droppedAxisCube.U, 'U', TEST_SUBDIRECTORY), ], expectedError: 'Image shapes or axes are not consistent!', }, { title: 'Case 2: Q and axis-degeneracy Q, duplicated Stokes type', stokesFiles: [ - stokesFile(stokesCube.Q, 'Q', testSubdirectory), - stokesFile(droppedAxisCube.Q, 'Q', testSubdirectory), + stokesFile(stokesCube.Q, 'Q', TEST_SUBDIRECTORY), + stokesFile(droppedAxisCube.Q, 'Q', TEST_SUBDIRECTORY), ], expectedError: 'Duplicate Stokes type found!', }, { title: 'Case 3: a single file, too few to concatenate', - stokesFiles: [stokesFile(stokesCube.Q, 'Q', testSubdirectory)], + stokesFiles: [stokesFile(stokesCube.Q, 'Q', TEST_SUBDIRECTORY)], expectedError: 'Need at least two files to concatenate!', }, { title: 'Case 4: a FITS image and a CASA image, mixed file types', stokesFiles: [ - stokesFile(stokesCube.Q, 'Q', testSubdirectory), - stokesFile(casaImage, 'U', testSubdirectory), + stokesFile(stokesCube.Q, 'Q', TEST_SUBDIRECTORY), + stokesFile(casaImage, 'U', TEST_SUBDIRECTORY), ], expectedError: 'Different file types can not be concatenated!', }, { title: 'Case 5: I, Q and V, a hypercube with a gap in the Stokes axis', stokesFiles: [ - stokesFile(stokesCube.I, 'I', testSubdirectory), - stokesFile(stokesCube.Q, 'Q', testSubdirectory), - stokesFile(stokesCube.V, 'V', testSubdirectory), + stokesFile(stokesCube.I, 'I', TEST_SUBDIRECTORY), + stokesFile(stokesCube.Q, 'Q', TEST_SUBDIRECTORY), + stokesFile(stokesCube.V, 'V', TEST_SUBDIRECTORY), ], expectedError: 'Hypercube IQV is not allowed!', }, { title: 'Case 6: a file which is not on disk', stokesFiles: [ - stokesFile(stokesCube.Q, 'Q', testSubdirectory), - stokesFile(missingImage, 'U', testSubdirectory), + stokesFile(stokesCube.Q, 'Q', TEST_SUBDIRECTORY), + stokesFile(missingImage, 'U', TEST_SUBDIRECTORY), ], expectedError: `${missingImage} does not exist.`, }, @@ -102,7 +101,10 @@ let assertItem: AssertItem = { validConcat: { fileId: 0, renderMode: CARTA.RenderMode.RASTER, - stokesFiles: [stokesFile(stokesCube.Q, 'Q', testSubdirectory), stokesFile(stokesCube.U, 'U', testSubdirectory)], + stokesFiles: [ + stokesFile(stokesCube.Q, 'Q', TEST_SUBDIRECTORY), + stokesFile(stokesCube.U, 'U', TEST_SUBDIRECTORY), + ], }, validConcatName: 'IRCp10216_sci.spw0.cube.hypercube_QU.manual.pbcor.fits', setCursor: { x: 128, y: 128 }, @@ -120,14 +122,13 @@ let assertItem: AssertItem = { }, }; -let basepath: string; - // The backend must have sent nothing beyond the CONCAT_STOKES_FILES_ACK itself. A concatenation // which was refused must not go on to stream REGION_HISTOGRAM_DATA or raster data for an image it -// never opened. +// never opened. That ack is the only message it is allowed to draw, and QUIET_TIME is how long the +// backend is watched for a further one. async function assertOnlyTheAckArrived(messageCountBeforeRequest: number) { const msgController = MessageController.Instance; - await new Promise((resolve) => setTimeout(resolve, quietTime)); + await new Promise((resolve) => setTimeout(resolve, QUIET_TIME)); expect(msgController.messageReceiving()).toEqual(messageCountBeforeRequest + 1); } @@ -149,23 +150,17 @@ async function assertFileIdIsFree(fileId: number) { describe('CONCAT_ERROR_MESSAGE test: incompatible Stokes images are refused with a message', () => { const msgController = MessageController.Instance; beforeAll(async () => { - await msgController.connect(testServerUrl); - }, connectTimeout); + await msgController.connect(TEST_SERVER_URL); + }, CONNECTION_TIMEOUT); checkConnection(); test(`Get the base path and prefix the Stokes file directories with it |`, async () => { - const fileListResponse = await msgController.getFileList('$BASE', 0); - basepath = fileListResponse.directory; - assertItem.fileList.directory = basepath + '/' + assertItem.fileList.directory; - assertItem.errorCases.forEach((errorCase) => { - errorCase.stokesFiles.forEach((request) => { - request.directory = basepath + '/' + request.directory; - }); - }); - assertItem.validConcat.stokesFiles!.forEach((request) => { - request.directory = basepath + '/' + request.directory; - }); + await assertBasePath([ + assertItem.fileList, + ...assertItem.errorCases.flatMap((errorCase) => errorCase.stokesFiles), + ...assertItem.validConcat.stokesFiles!, + ]); }); // Case 6 asks for a file which is not on disk, so the test first has to know that the files the @@ -187,7 +182,7 @@ describe('CONCAT_ERROR_MESSAGE test: incompatible Stokes images are refused with let messageCountBeforeRequest: number; test( - `(Step 1) CONCAT_STOKES_FILES should be refused with "${errorCase.expectedError}" within ${concatStokeTimeout} ms | `, + `(Step 1) CONCAT_STOKES_FILES should be refused with "${errorCase.expectedError}" within ${CONCAT_STOKES_TIMEOUT} ms | `, async () => { msgController.closeFile(-1); messageCountBeforeRequest = msgController.messageReceiving(); @@ -202,7 +197,7 @@ describe('CONCAT_ERROR_MESSAGE test: incompatible Stokes images are refused with ) ).rejects.toContain(errorCase.expectedError); }, - concatStokeTimeout + CONCAT_STOKES_TIMEOUT ); test(`(Step 2) The refused CONCAT_STOKES_FILES should draw no other message | `, async () => { @@ -219,7 +214,7 @@ describe('CONCAT_ERROR_MESSAGE test: incompatible Stokes images are refused with let concatStokesResponse: CARTA.IConcatStokesFilesAck; test( - `(Step 1) CONCAT_STOKES_FILES_ACK should arrive within ${concatStokeTimeout} ms | `, + `(Step 1) CONCAT_STOKES_FILES_ACK should arrive within ${CONCAT_STOKES_TIMEOUT} ms | `, async () => { msgController.closeFile(-1); const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); @@ -231,7 +226,7 @@ describe('CONCAT_ERROR_MESSAGE test: incompatible Stokes images are refused with const regionHistogramData = await regionHistogramDataStream; expect(regionHistogramData[0].fileId).toEqual(assertItem.validConcat.fileId); }, - concatStokeTimeout + CONCAT_STOKES_TIMEOUT ); // The refusals above each leave the connector holding the loaders they opened until diff --git a/src/test/CONCAT_STOKES_IMAGES.test.ts b/src/test/CONCAT_STOKES_IMAGES.test.ts index d1b79131..e596ab4f 100644 --- a/src/test/CONCAT_STOKES_IMAGES.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES.test.ts @@ -1,5 +1,4 @@ import { CARTA } from 'carta-protobuf'; -import config from './config.json'; import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; import { @@ -13,13 +12,15 @@ import { stokesFile, stokesImages, } from './ConcatStokesHelpers'; - -let testServerUrl: string = config.serverURL0; -let testSubdirectory: string = config.path.QA; -let connectTimeout: number = config.timeout.connection; -let openFileTimeout = config.timeout.openFile; -let concatStokeTimeout = config.timeout.concatStokes; -let changeChannelTimeout = config.timeout.changeChannel; +import { + CHANGE_CHANNEL_TIMEOUT, + CONCAT_STOKES_TIMEOUT, + CONNECTION_TIMEOUT, + OPEN_FILE_TIMEOUT, + TEST_SERVER_URL, + TEST_SUBDIRECTORY, + assertBasePath, +} from './CommonHelpers'; interface AssertItem { fileList: CARTA.IFileListRequest; @@ -31,7 +32,7 @@ interface AssertItem { } let assertItem: AssertItem = { - fileList: { directory: testSubdirectory }, + fileList: { directory: TEST_SUBDIRECTORY }, stokesImages: stokesImages({ I: 'IRCp10216_sci.spw0.cube.I.manual.pbcor.fits', Q: 'IRCp10216_sci.spw0.cube.Q.manual.pbcor.fits', @@ -76,20 +77,16 @@ let assertItem: AssertItem = { imageShape: { dimensions: 4, width: 256, height: 256, depth: 480 }, }; -let basepath: string; - describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into single image', () => { const msgController = MessageController.Instance; beforeAll(async () => { - await msgController.connect(testServerUrl); - }, connectTimeout); + await msgController.connect(TEST_SERVER_URL); + }, CONNECTION_TIMEOUT); checkConnection(); test(`Get the base path and prefix the image directory with it |`, async () => { - const fileListResponse = await msgController.getFileList('$BASE', 0); - basepath = fileListResponse.directory; - assertItem.fileList.directory = basepath + '/' + assertItem.fileList.directory; + await assertBasePath([assertItem.fileList]); }); assertItem.concatCases.forEach((concatCase) => { @@ -107,7 +104,7 @@ describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into si concatCase.requestOrder.forEach((stokesLetter, index) => { test( - `(Step 2-${index + 1}) FILE_INFO_RESPONSE for the Stokes ${stokesLetter} cube should arrive within ${openFileTimeout} ms | `, + `(Step 2-${index + 1}) FILE_INFO_RESPONSE for the Stokes ${stokesLetter} cube should arrive within ${OPEN_FILE_TIMEOUT} ms | `, async () => { const stokesImage = assertItem.stokesImages[stokesLetter]; const fileInfoResponse = await msgController.getFileInfo( @@ -118,12 +115,12 @@ describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into si expect(fileInfoResponse.success).toEqual(true); expect(fileInfoResponse.fileInfo!.name).toEqual(stokesImage.file); }, - openFileTimeout + OPEN_FILE_TIMEOUT ); }); test( - `(Step 3) CONCAT_STOKES_FILES_ACK and REGION_HISTOGRAM_DATA should arrive within ${concatStokeTimeout} ms | `, + `(Step 3) CONCAT_STOKES_FILES_ACK and REGION_HISTOGRAM_DATA should arrive within ${CONCAT_STOKES_TIMEOUT} ms | `, async () => { msgController.closeFile(-1); const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); @@ -148,7 +145,7 @@ describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into si assertItem.fileId ); }, - concatStokeTimeout + CONCAT_STOKES_TIMEOUT ); test(`(Step 4) CONCAT_STOKES_FILES_ACK should describe the hypercube | `, () => { @@ -165,11 +162,11 @@ describe('CONCAT_STOKES_IMAGES test: concatenate different stokes images into si concatCase.expectedPlanes.slice(1).forEach((stokesLetter, index) => { const stokes = index + 1; test( - `(Step 5-${stokes}) Stokes plane ${stokes} should hold the ${stokesLetter} image within ${changeChannelTimeout} ms | `, + `(Step 5-${stokes}) Stokes plane ${stokes} should hold the ${stokesLetter} image within ${CHANGE_CHANNEL_TIMEOUT} ms | `, async () => { await assertStokesPlane(assertItem.fileId, stokes, stokesLetter); }, - changeChannelTimeout + CHANGE_CHANNEL_TIMEOUT ); }); }); diff --git a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts index df82134a..35c6bed7 100644 --- a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts @@ -1,5 +1,4 @@ import { CARTA } from 'carta-protobuf'; -import config from './config.json'; import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; import { @@ -13,13 +12,15 @@ import { stokesFile, stokesImages, } from './ConcatStokesHelpers'; - -let testServerUrl: string = config.serverURL0; -let testSubdirectory: string = config.path.QA; -let connectTimeout: number = config.timeout.connection; -let openFileTimeout = config.timeout.openFile; -let concatStokeTimeout = config.timeout.concatStokes; -let changeChannelTimeout = config.timeout.changeChannel; +import { + CHANGE_CHANNEL_TIMEOUT, + CONCAT_STOKES_TIMEOUT, + CONNECTION_TIMEOUT, + OPEN_FILE_TIMEOUT, + TEST_SERVER_URL, + TEST_SUBDIRECTORY, + assertBasePath, +} from './CommonHelpers'; interface AssertItem { fileList: CARTA.IFileListRequest; @@ -34,7 +35,7 @@ interface AssertItem { } let assertItem: AssertItem = { - fileList: { directory: testSubdirectory }, + fileList: { directory: TEST_SUBDIRECTORY }, stokesImages: stokesImages({ I: 'IRCp10216_sci.spw0.cube.I.dropdeg.manual.pbcor.fits', Q: 'IRCp10216_sci.spw0.cube.Q.dropdeg.manual.pbcor.fits', @@ -79,20 +80,16 @@ let assertItem: AssertItem = { hypercubeShape: { dimensions: 4, width: 256, height: 256, depth: 480 }, }; -let basepath: string; - describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis-degeneracy stokes images into a single image.', () => { const msgController = MessageController.Instance; beforeAll(async () => { - await msgController.connect(testServerUrl); - }, connectTimeout); + await msgController.connect(TEST_SERVER_URL); + }, CONNECTION_TIMEOUT); checkConnection(); test(`Get the base path and prefix the image directory with it |`, async () => { - const fileListResponse = await msgController.getFileList('$BASE', 0); - basepath = fileListResponse.directory; - assertItem.fileList.directory = basepath + '/' + assertItem.fileList.directory; + await assertBasePath([assertItem.fileList]); }); assertItem.concatCases.forEach((concatCase) => { @@ -110,7 +107,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- concatCase.requestOrder.forEach((stokesLetter, index) => { test( - `(Step 2-${index + 1}) FILE_INFO_RESPONSE for the Stokes ${stokesLetter} cube should show a dropped Stokes axis within ${openFileTimeout} ms | `, + `(Step 2-${index + 1}) FILE_INFO_RESPONSE for the Stokes ${stokesLetter} cube should show a dropped Stokes axis within ${OPEN_FILE_TIMEOUT} ms | `, async () => { const stokesImage = assertItem.stokesImages[stokesLetter]; const fileInfoResponse = await msgController.getFileInfo( @@ -129,12 +126,12 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- expect(fileInfoExtended.depth).toEqual(assertItem.inputShape.depth); expect(fileInfoExtended.stokes).toEqual(assertItem.inputShape.stokes); }, - openFileTimeout + OPEN_FILE_TIMEOUT ); }); test( - `(Step 3) CONCAT_STOKES_FILES_ACK and REGION_HISTOGRAM_DATA should arrive within ${concatStokeTimeout} ms | `, + `(Step 3) CONCAT_STOKES_FILES_ACK and REGION_HISTOGRAM_DATA should arrive within ${CONCAT_STOKES_TIMEOUT} ms | `, async () => { msgController.closeFile(-1); const regionHistogramDataStream = Stream(CARTA.RegionHistogramData, 1); @@ -159,7 +156,7 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- assertItem.fileId ); }, - concatStokeTimeout + CONCAT_STOKES_TIMEOUT ); // The inputs were three-dimensional; the Stokes axis of the hypercube is one DoConcat @@ -178,11 +175,11 @@ describe('CONCAT_STOKES_IMAGES_AXIS_DEGENERACY test: Concatenate different axis- concatCase.expectedPlanes.slice(1).forEach((stokesLetter, index) => { const stokes = index + 1; test( - `(Step 5-${stokes}) Stokes plane ${stokes} should hold the ${stokesLetter} image within ${changeChannelTimeout} ms | `, + `(Step 5-${stokes}) Stokes plane ${stokes} should hold the ${stokesLetter} image within ${CHANGE_CHANNEL_TIMEOUT} ms | `, async () => { await assertStokesPlane(assertItem.fileId, stokes, stokesLetter); }, - changeChannelTimeout + CHANGE_CHANNEL_TIMEOUT ); }); }); diff --git a/src/test/CommonHelpers.ts b/src/test/CommonHelpers.ts index 89e41e44..b583696f 100644 --- a/src/test/CommonHelpers.ts +++ b/src/test/CommonHelpers.ts @@ -17,6 +17,8 @@ export const READ_FILE_TIMEOUT: number = config.timeout.readFile; export const READ_LARGE_IMAGE_TIMEOUT: number = config.timeout.readLargeImage; export const OPEN_CATALOG_LARGE_TIMEOUT: number = config.timeout.openCatalogLarge; export const PLAY_ANIMATOR_TIMEOUT: number = config.timeout.playAnimator; +export const CONCAT_STOKES_TIMEOUT: number = config.timeout.concatStokes; +export const CHANGE_CHANNEL_TIMEOUT: number = config.timeout.changeChannel; // How long silence is waited for, where a request draws no acknowledgement of its own and the // only thing which can be observed after it is that nothing arrives. export const QUIET_TIME: number = config.timeout.messageEvent;