Task Summary
ResultExportService (amber/src/main/scala/org/apache/texera/web/service/ResultExportService.scala) is at ~6% — 160 of 170 tracked lines are unhit. The existing ResultExportServiceSpec only covers parseOperators and validateExportRequest. The five streamDocumentAs* writers look storage-bound but are not: each takes a VirtualDocument[Tuple] parameter, and VirtualDocument is an ordinary abstract class whose only abstract members are getURI and clear() — every accessor defaults to throw new NotImplementedError. A small in-spec fake overriding getCount / get() / getRange / asInputStream() makes the whole export-encoding layer testable against a ByteArrayOutputStream, with no Iceberg, MinIO or DB.
Behavior to add
Extend amber/src/test/scala/org/apache/texera/web/service/ResultExportServiceSpec.scala. The service constructs cheaply — the spec already does new ResultExportService(WorkflowIdentity(1L), computingUnitId = 0). Reach the private writers with PrivateMethodTester (the idiom WorkflowExecutionsResourceSpec already uses). For the fake document, mirror MinimalDoc in common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/VirtualDocumentSpec.scala — override only what each test needs.
streamDocumentAsCSV (private; doc, outputStream, maybeHeaders: Option[List[String]])
getCount == 0 → returns without writing a byte.
maybeHeaders = None → header row is inferred from firstRow.getSchema.getAttributeNames, and that first row is also written to the body.
maybeHeaders = Some(hdrs) → hdrs is written as the header and the iterator's first row is not consumed, so it still appears in the body. This asymmetry between the two branches is the off-by-one worth pinning.
- A document larger than
Constants.CHUNK_SIZE (10) exercises the buffered chunk loop and the per-chunk flush().
streamCellData (private; out, request, operatorDocument)
rowIndex >= getCount → WebApplicationException with message Invalid rowIndex (5). Total rows: 2.
columnIndex >= selectedRow.getFields.length → WebApplicationException with the Invalid columnIndex message.
- Happy path writes exactly the UTF-8 bytes of the selected cell.
convertFieldToBytes (private) — three branches: an Array[Byte] passes through unchanged; a String encodes as UTF-8 (use a multi-byte character to pin the charset); anything else goes through toString.
streamDocumentAsHTML (private) — writes results.head.getField(0).toString as UTF-8. An empty document hits an unguarded .head and throws NoSuchElementException; assert that rather than leaving it undocumented.
streamDocumentAsParquetZip (private) — with a fake whose asInputStream() returns a ByteArrayInputStream, assert the bytes are copied through verbatim and the source stream is closed.
streamDocumentAsArrow (private) — runs entirely in-process on a RootAllocator; assert the Arrow stream round-trips the tuples back. Include only if it keeps the PR reviewable; it is the largest single block.
NonClosingOutputStream.close — asserts the wrapped stream is not closed.
Out of scope
getOperatorDocument, saveToDatasets, saveStreamToDataset and the exportSingleOperatorToDataset happy path need DocumentFactory.openDocument and a live file-service HTTP endpoint — leave them uncovered. Note fileServiceUploadOneFileToDatasetEndpoint is a lazy val defaulting to http://localhost:9092/..., so any test that reaches saveToDatasets will attempt a real socket connection.
generateFileName embeds LocalDateTime.now() in the name. If it is covered at all, assert only the stable parts — the ${workflowName}-op$operatorId-v$latestVersion- prefix, the parquet → .zip extension mapping, and that StringUtils.replaceEach strips / and \ from the workflow name. Never assert the full filename.
Task Type
Task Summary
ResultExportService(amber/src/main/scala/org/apache/texera/web/service/ResultExportService.scala) is at ~6% — 160 of 170 tracked lines are unhit. The existingResultExportServiceSpeconly coversparseOperatorsandvalidateExportRequest. The fivestreamDocumentAs*writers look storage-bound but are not: each takes aVirtualDocument[Tuple]parameter, andVirtualDocumentis an ordinary abstract class whose only abstract members aregetURIandclear()— every accessor defaults tothrow new NotImplementedError. A small in-spec fake overridinggetCount/get()/getRange/asInputStream()makes the whole export-encoding layer testable against aByteArrayOutputStream, with no Iceberg, MinIO or DB.Behavior to add
Extend
amber/src/test/scala/org/apache/texera/web/service/ResultExportServiceSpec.scala. The service constructs cheaply — the spec already doesnew ResultExportService(WorkflowIdentity(1L), computingUnitId = 0). Reach the private writers withPrivateMethodTester(the idiomWorkflowExecutionsResourceSpecalready uses). For the fake document, mirrorMinimalDocincommon/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/VirtualDocumentSpec.scala— override only what each test needs.streamDocumentAsCSV(private;doc,outputStream,maybeHeaders: Option[List[String]])getCount == 0→ returns without writing a byte.maybeHeaders = None→ header row is inferred fromfirstRow.getSchema.getAttributeNames, and that first row is also written to the body.maybeHeaders = Some(hdrs)→hdrsis written as the header and the iterator's first row is not consumed, so it still appears in the body. This asymmetry between the two branches is the off-by-one worth pinning.Constants.CHUNK_SIZE(10) exercises the buffered chunk loop and the per-chunkflush().streamCellData(private;out,request,operatorDocument)rowIndex >= getCount→WebApplicationExceptionwith messageInvalid rowIndex (5). Total rows: 2.columnIndex >= selectedRow.getFields.length→WebApplicationExceptionwith theInvalid columnIndexmessage.convertFieldToBytes(private) — three branches: anArray[Byte]passes through unchanged; aStringencodes as UTF-8 (use a multi-byte character to pin the charset); anything else goes throughtoString.streamDocumentAsHTML(private) — writesresults.head.getField(0).toStringas UTF-8. An empty document hits an unguarded.headand throwsNoSuchElementException; assert that rather than leaving it undocumented.streamDocumentAsParquetZip(private) — with a fake whoseasInputStream()returns aByteArrayInputStream, assert the bytes are copied through verbatim and the source stream is closed.streamDocumentAsArrow(private) — runs entirely in-process on aRootAllocator; assert the Arrow stream round-trips the tuples back. Include only if it keeps the PR reviewable; it is the largest single block.NonClosingOutputStream.close— asserts the wrapped stream is not closed.Out of scope
getOperatorDocument,saveToDatasets,saveStreamToDatasetand theexportSingleOperatorToDatasethappy path needDocumentFactory.openDocumentand a live file-service HTTP endpoint — leave them uncovered. NotefileServiceUploadOneFileToDatasetEndpointis alazy valdefaulting tohttp://localhost:9092/..., so any test that reachessaveToDatasetswill attempt a real socket connection.generateFileNameembedsLocalDateTime.now()in the name. If it is covered at all, assert only the stable parts — the${workflowName}-op$operatorId-v$latestVersion-prefix, theparquet→.zipextension mapping, and thatStringUtils.replaceEachstrips/and\from the workflow name. Never assert the full filename.Task Type