diff --git a/ICD_test_stages/file_browser.tests b/ICD_test_stages/file_browser.tests index dc67fb0..2450a85 100644 --- a/ICD_test_stages/file_browser.tests +++ b/ICD_test_stages/file_browser.tests @@ -8,3 +8,6 @@ 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_STOKES_IMAGES_AXIS_DEGENERACY.test.ts +src/test/CONCAT_ERROR_MESSAGE.test.ts diff --git a/docs/source/concat_stokes.rst b/docs/source/concat_stokes.rst index 87baf49..d466647 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. + +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** +**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: +:red-text:`Check 7:` the Stokes axis should read Q, U — plane 0 matches Stokes Q, plane 1 matches Stokes U. - - histograms.binWidth = 0.00016267175669781864 - - histograms.firstBinCenter = -0.018377140164375305 - - histograms.numBins = 256 - - histograms.mean = -0.00003742659352908538 - - histograms.stdDev = 0.0038693415380174558 - -**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, 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): @@ -151,50 +189,76 @@ This test is identical in structure and expected results to CONCAT_STOKES_IMAGES - ``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 ~~~~~~~~~~~~~~~~~~~~ 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 +273,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 +298,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 84451ee..08313fa 100644 --- a/src/test/CONCAT_ERROR_MESSAGE.test.ts +++ b/src/test/CONCAT_ERROR_MESSAGE.test.ts @@ -1,123 +1,261 @@ import { CARTA } from 'carta-protobuf'; -import config from './config.json'; -import { checkConnection } from './MyClient'; +import { checkConnection, Stream } from './MyClient'; import { MessageController } from './MessageController'; +import { stokesFile } from './ConcatStokesHelpers'; +import { + CONCAT_STOKES_TIMEOUT, + CONNECTION_TIMEOUT, + QUIET_TIME, + TEST_SERVER_URL, + TEST_SUBDIRECTORY, + assertBasePath, +} from './CommonHelpers'; -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; +/** 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. +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: TEST_SUBDIRECTORY }, + errorCases: [ + { + title: 'Case 1: Q and axis-degeneracy U, image shapes inconsistent', + stokesFiles: [ + 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', 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', 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', 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', 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', TEST_SUBDIRECTORY), + stokesFile(missingImage, 'U', TEST_SUBDIRECTORY), + ], + expectedError: `${missingImage} 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, - }, - { - directory: testSubdirectory, - hdu: '', - file: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', - polarizationType: 3, - }, + stokesFile(stokesCube.Q, 'Q', TEST_SUBDIRECTORY), + stokesFile(stokesCube.U, 'U', TEST_SUBDIRECTORY), ], }, - 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', () => { +// 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. 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, QUIET_TIME)); + 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); + await msgController.connect(TEST_SERVER_URL); + }, CONNECTION_TIMEOUT); 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 () => { + await assertBasePath([ + assertItem.fileList, + ...assertItem.errorCases.flatMap((errorCase) => errorCase.stokesFiles), + ...assertItem.validConcat.stokesFiles!, + ]); + }); - 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((request) => request.file!) + .filter((file) => file !== missingImage) + .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 ${CONCAT_STOKES_TIMEOUT} 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); + }, + CONCAT_STOKES_TIMEOUT + ); + + 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 ${CONCAT_STOKES_TIMEOUT} 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 + CONCAT_STOKES_TIMEOUT ); + + // 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()); diff --git a/src/test/CONCAT_STOKES_IMAGES.test.ts b/src/test/CONCAT_STOKES_IMAGES.test.ts index 63a6c16..e596ab4 100644 --- a/src/test/CONCAT_STOKES_IMAGES.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES.test.ts @@ -1,838 +1,174 @@ 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; - -interface ConcatStokesFilesAckExt extends CARTA.IConcatStokesFilesAck { - OpenFileAckBeamLength: number; -} +import { + ConcatCase, + ImageShape, + StokesImage, + StokesLetter, + assertHistogramMatchesStokesImage, + assertHypercubeAck, + assertStokesPlane, + stokesFile, + stokesImages, +} from './ConcatStokesHelpers'; +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; - 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[]; - precisionDigits: number; + fileList: CARTA.IFileListRequest; + stokesImages: Record; + concatCases: ConcatCase[]; + fileId: number; + renderMode: CARTA.RenderMode; + imageShape: ImageShape; } let assertItem: AssertItem = { - filelist: { directory: testSubdirectory }, - fileInfoReq: [ - { - 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, - }, - { - 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, - }, - ], - }, - 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, - }, - ], - }, - 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, - }, - ], - }, - RegionHistogramDataResponseQUV: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.00016267175669781864, - firstBinCenter: -0.018377140164375305, - numBins: 256, - stdDev: 0.0038693415380174558, - mean: -0.00003742659352908538, - }, - }, - ConcatResponse: [ + fileList: { directory: TEST_SUBDIRECTORY }, + 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: [ { - 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', }, ], - precisionDigits: 6, + 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 }, }; 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(); - 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 () => { + await assertBasePath([assertItem.fileList]); }); - 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 ${OPEN_FILE_TIMEOUT} 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); + }, + OPEN_FILE_TIMEOUT + ); + }); - 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 ${CONCAT_STOKES_TIMEOUT} 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) => + stokesFile( + assertItem.stokesImages[stokesLetter].file, + stokesLetter, + assertItem.fileList.directory! + ) + ), + assertItem.fileId, + assertItem.renderMode ); - 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 = 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 + 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], + assertItem.fileId ); - expect(FileInfoResponse.success).toEqual(true); }, - openFileTimeout + CONCAT_STOKES_TIMEOUT ); - }); - 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); - }, - }); + test(`(Step 4) CONCAT_STOKES_FILES_ACK should describe the hypercube | `, () => { + assertHypercubeAck(concatStokesResponse, { + fileId: assertItem.fileId, + name: concatCase.hypercubeName, + shape: assertItem.imageShape, + planes: concatCase.expectedPlanes, }); - 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 - ); + }); + + // Plane 0 was covered by the histogram which came with the ack. The remaining planes are + // 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 ${CHANGE_CHANNEL_TIMEOUT} ms | `, + async () => { + await assertStokesPlane(assertItem.fileId, stokes, stokesLetter); + }, + 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 cbb20e8..35c6bed 100644 --- a/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts +++ b/src/test/CONCAT_STOKES_IMAGES_AXIS_DEGENERACY.test.ts @@ -1,836 +1,187 @@ 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; - -interface ConcatStokesFilesAckExt extends CARTA.IConcatStokesFilesAck { - OpenFileAckBeamLength: number; -} +import { + ConcatCase, + ImageShape, + StokesImage, + StokesLetter, + assertHistogramMatchesStokesImage, + assertHypercubeAck, + assertStokesPlane, + stokesFile, + stokesImages, +} from './ConcatStokesHelpers'; +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; - 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: ImageShape & { stokes: number }; + hypercubeShape: ImageShape; } let assertItem: AssertItem = { - filelist: { directory: testSubdirectory }, - fileInfoReq: [ - { - 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, - }, - ], - }, - 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, - }, - ], - }, - 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, - }, - ], - }, - 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, - }, - ], - }, - RegionHistogramDataResponseQUV: { - progress: 1, - regionId: -1, - config: { - numBins: -1, - }, - histograms: { - binWidth: 0.00016267175669781864, - firstBinCenter: -0.018377140164375305, - numBins: 256, - stdDev: 0.0038693415380174558, - mean: -0.00003742659352908538, - }, - }, - ConcatResponse: [ + 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', + U: 'IRCp10216_sci.spw0.cube.U.dropdeg.manual.pbcor.fits', + V: 'IRCp10216_sci.spw0.cube.V.dropdeg.manual.pbcor.fits', + }), + 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 }, }; 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(); - 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 () => { + await assertBasePath([assertItem.fileList]); }); - 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 show a dropped Stokes axis within ${OPEN_FILE_TIMEOUT} 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); + }, + OPEN_FILE_TIMEOUT + ); + }); - 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 ${CONCAT_STOKES_TIMEOUT} 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) => + stokesFile( + assertItem.stokesImages[stokesLetter].file, + stokesLetter, + assertItem.fileList.directory! + ) + ), + assertItem.fileId, + assertItem.renderMode ); - 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 = 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 + 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], + assertItem.fileId ); - expect(FileInfoResponse.success).toEqual(true); }, - openFileTimeout + CONCAT_STOKES_TIMEOUT ); - }); - 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); - }, - }); + // 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 | `, () => { + assertHypercubeAck(concatStokesResponse, { + fileId: assertItem.fileId, + name: concatCase.hypercubeName, + shape: assertItem.hypercubeShape, + planes: concatCase.expectedPlanes, }); - 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 - ); + }); + + // Plane 0 was covered by the histogram which came with the ack. The remaining planes are + // 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 ${CHANGE_CHANNEL_TIMEOUT} ms | `, + async () => { + await assertStokesPlane(assertItem.fileId, stokes, stokesLetter); + }, + CHANGE_CHANNEL_TIMEOUT + ); + }); }); }); diff --git a/src/test/CommonHelpers.ts b/src/test/CommonHelpers.ts index 89e41e4..b583696 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; diff --git a/src/test/ConcatStokesHelpers.ts b/src/test/ConcatStokesHelpers.ts new file mode 100644 index 0000000..76fc763 --- /dev/null +++ b/src/test/ConcatStokesHelpers.ts @@ -0,0 +1,188 @@ +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: a file, and the Stokes type it is offered to the + * backend as. The two are given separately because a request may name a file which is not the + * Stokes cube it claims to be, or which is not there at all, and CONCAT_ERROR_MESSAGE is built + * out of exactly those. + */ +export function stokesFile(file: string, stokesLetter: StokesLetter, directory: string): CARTA.IStokesFile { + return { + directory: directory, + hdu: '', + file: file, + polarizationType: POLARIZATION_TYPES[stokesLetter], + }; +} + +/** + * 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); +}