aglinxinyuan commented on code in PR #6598:
URL: https://github.com/apache/texera/pull/6598#discussion_r3613702916
##########
frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts:
##########
@@ -136,6 +140,287 @@ describe("WorkflowResultExportService", () => {
expect(service.hasResultToExportOnHighlightedOperators).toBe(false);
expect(service.hasResultToExportOnAllOperators.value).toBe(false);
});
+
+ // ---- Test helpers
----------------------------------------------------------
+
+ function enableExport(): void {
+ const config = TestBed.inject(GuiConfigService) as unknown as
MockGuiConfigService;
+ config.setConfig({ exportExecutionResultEnabled: true });
+ }
+
+ function makeUnit(): DashboardWorkflowComputingUnit {
+ return { computingUnit: { cuid: 1, type: "local" } } as unknown as
DashboardWorkflowComputingUnit;
+ }
+
+ /** Stubs the export-flow methods the real DownloadService exposes but the
base spy omits. */
+ function stubDownloadService(overrides?: {
+ downloadability?: Record<string, string[]>;
+ datasetResponse?: HttpResponse<unknown>;
+ datasetError?: unknown;
+ }): {
+ getWorkflowResultDownloadability: ReturnType<typeof vi.fn>;
+ exportWorkflowResultToDataset: ReturnType<typeof vi.fn>;
+ exportWorkflowResultToLocal: ReturnType<typeof vi.fn>;
+ } {
+ const getWorkflowResultDownloadability =
vi.fn().mockReturnValue(of(overrides?.downloadability ?? {}));
+ const exportWorkflowResultToDataset = vi
+ .fn()
+ .mockReturnValue(
+ overrides?.datasetError !== undefined
+ ? throwError(() => overrides.datasetError)
+ : of(overrides?.datasetResponse ?? new HttpResponse({ body: {
status: "success", message: "ok" } }))
+ );
+ const exportWorkflowResultToLocal = vi.fn();
+ (downloadServiceSpy as any).getWorkflowResultDownloadability =
getWorkflowResultDownloadability;
+ (downloadServiceSpy as any).exportWorkflowResultToDataset =
exportWorkflowResultToDataset;
+ (downloadServiceSpy as any).exportWorkflowResultToLocal =
exportWorkflowResultToLocal;
+ (workflowResultServiceSpy as any).determineOutputExtension =
vi.fn().mockReturnValue("csv");
+ return { getWorkflowResultDownloadability, exportWorkflowResultToDataset,
exportWorkflowResultToLocal };
+ }
+
+ describe("computeRestrictionAnalysis", () => {
+ it("returns an empty restriction map without hitting the backend when the
workflow has no id", () => {
+ workflowActionServiceSpy.getWorkflow.mockReturnValue({ wid: undefined }
as any);
+ const download = stubDownloadService({ downloadability: { op1: ["ds1"] }
});
+
+ let result: WorkflowResultDownloadability | undefined;
+ service.computeRestrictionAnalysis().subscribe(r => (result = r));
+
+ expect(result?.restrictedOperatorMap.size).toBe(0);
+ expect(download.getWorkflowResultDownloadability).not.toHaveBeenCalled();
+ });
+
+ it("maps the backend response into a Map of operatorId ->
Set<datasetLabel>", () => {
+ const download = stubDownloadService({
+ downloadability: { op1: ["ds1 ([email protected])"], op2: ["ds2
([email protected])", "ds3 ([email protected])"] },
+ });
+
+ let result: WorkflowResultDownloadability | undefined;
+ service.computeRestrictionAnalysis().subscribe(r => (result = r));
+
+
expect(download.getWorkflowResultDownloadability).toHaveBeenCalledWith("workflow1");
+ expect(result?.restrictedOperatorMap.get("op1")).toEqual(new Set(["ds1
([email protected])"]));
+ expect(result?.restrictedOperatorMap.get("op2")).toEqual(new Set(["ds2
([email protected])", "ds3 ([email protected])"]));
+ });
+
+ it("falls back to an empty restriction map when the backend call errors",
() => {
+ const getWorkflowResultDownloadability =
vi.fn().mockReturnValue(throwError(() => new Error("boom")));
+ (downloadServiceSpy as any).getWorkflowResultDownloadability =
getWorkflowResultDownloadability;
+
+ let result: WorkflowResultDownloadability | undefined;
+ service.computeRestrictionAnalysis().subscribe(r => (result = r));
+
+ expect(result).toBeInstanceOf(WorkflowResultDownloadability);
+ expect(result?.restrictedOperatorMap.size).toBe(0);
+ });
+ });
+
+ describe("export flow (exportWorkflowExecutionResult / performExport)", ()
=> {
+ it("does nothing when export is disabled in the gui config", () => {
+ const download = stubDownloadService();
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [1], 0, 0, "file",
true, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.loading).not.toHaveBeenCalled();
+ expect(notificationServiceSpy.error).not.toHaveBeenCalled();
+ expect(download.exportWorkflowResultToDataset).not.toHaveBeenCalled();
+ });
+
+ it("errors when the computing unit is null", () => {
+ enableExport();
+ const download = stubDownloadService();
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [1], 0, 0, "file",
true, "dataset", null);
+
+ expect(notificationServiceSpy.error).toHaveBeenCalledWith(
+ "Cannot export result: computing unit is not available"
+ );
+ expect(notificationServiceSpy.loading).not.toHaveBeenCalled();
+ expect(download.exportWorkflowResultToDataset).not.toHaveBeenCalled();
+ });
+
+ it("errors when the workflow id is not available", () => {
+ enableExport();
+ workflowActionServiceSpy.getWorkflow.mockReturnValue({ wid: undefined }
as any);
+ const download = stubDownloadService();
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [1], 0, 0, "file",
true, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.error).toHaveBeenCalledWith("Cannot export
result: workflow ID is not available");
+ expect(download.exportWorkflowResultToDataset).not.toHaveBeenCalled();
+ });
+
+ it("does nothing when no operators are selected (highlighted export with
empty selection)", () => {
+ enableExport();
+ const download = stubDownloadService();
+
jointGraphWrapperSpy.getCurrentHighlightedOperatorIDs.mockReturnValue([]);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [1], 0, 0, "file",
false, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.loading).not.toHaveBeenCalled();
+ expect(notificationServiceSpy.error).not.toHaveBeenCalled();
+ expect(download.exportWorkflowResultToDataset).not.toHaveBeenCalled();
+ });
+
+ it("errors (no export) when every selected operator is blocked by a
non-downloadable dataset", () => {
+ enableExport();
+ const download = stubDownloadService({ downloadability: { op1: ["ds1
([email protected])"] } });
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [1], 0, 0, "file",
true, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.error).toHaveBeenCalledWith(
+ "Cannot export result: selection depends on dataset(s) that are not
downloadable: ds1 ([email protected])"
+ );
+ expect(notificationServiceSpy.loading).not.toHaveBeenCalled();
+ expect(download.exportWorkflowResultToDataset).not.toHaveBeenCalled();
+ });
+
+ it("warns and still exports the unblocked operators when only some are
blocked", () => {
+ enableExport();
+ (notificationServiceSpy as any).warning = vi.fn();
+ const download = stubDownloadService({ downloadability: { op2: ["ds2
([email protected])"] } });
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }, {
operatorID: "op2" }] as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [7], 1, 2, "file",
true, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.warning).toHaveBeenCalledWith(
+ "Some operators were skipped because their results depend on
dataset(s) that are not downloadable" +
+ " (ds2 ([email protected]))"
+ );
+
expect(notificationServiceSpy.loading).toHaveBeenCalledWith("Exporting...");
+ expect(download.exportWorkflowResultToDataset).toHaveBeenCalledWith(
+ "csv",
+ "workflow1",
+ "wf",
+ [{ id: "op1", outputType: "csv" }],
+ [7],
+ 1,
+ 2,
+ "file",
+ expect.anything()
+ );
+ expect(notificationServiceSpy.success).toHaveBeenCalledWith("Result
exported successfully");
+ });
+
+ it("exports to dataset and reports success on a success response", () => {
+ enableExport();
+ (notificationServiceSpy as any).warning = vi.fn();
+ const download = stubDownloadService();
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [5], 0, 0, "file",
true, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.warning).not.toHaveBeenCalled();
+
expect(notificationServiceSpy.loading).toHaveBeenCalledWith("Exporting...");
+ expect(download.exportWorkflowResultToDataset).toHaveBeenCalledTimes(1);
+ expect(notificationServiceSpy.success).toHaveBeenCalledWith("Result
exported successfully");
+ });
+
+ it("reports the server message when the dataset export response is not
successful", () => {
+ enableExport();
+ stubDownloadService({
+ datasetResponse: new HttpResponse({ body: { status: "failure",
message: "quota exceeded" } }),
+ });
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [5], 0, 0, "file",
true, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.error).toHaveBeenCalledWith("quota
exceeded");
+ expect(notificationServiceSpy.success).not.toHaveBeenCalled();
+ });
+
+ it("reports a default error message when the dataset export response has
no body", () => {
+ enableExport();
+ stubDownloadService({ datasetResponse: new HttpResponse({}) });
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [5], 0, 0, "file",
true, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.error).toHaveBeenCalledWith("An error
occurred during export");
+ });
+
+ it("reports the extracted error message when the dataset export call
fails", () => {
+ enableExport();
+ stubDownloadService({ datasetError: { error: { message: "server
exploded" } } });
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("csv", "wf", [5], 0, 0, "file",
true, "dataset", makeUnit());
+
+ expect(notificationServiceSpy.error).toHaveBeenCalledWith(
+ "An error happened in exporting operator results: server exploded"
+ );
+ });
+
+ it("routes to the local-filesystem export when destination is 'local'", ()
=> {
+ enableExport();
+ const download = stubDownloadService();
+ texeraGraphSpy.getAllOperators.mockReturnValue([{ operatorID: "op1" }]
as any);
+
+ service.exportWorkflowExecutionResult("json", "wf", [], 3, 4,
"local-file", true, "local", makeUnit());
+
+
expect(notificationServiceSpy.loading).toHaveBeenCalledWith("Exporting...");
+ expect(download.exportWorkflowResultToLocal).toHaveBeenCalledWith(
+ "json",
+ "workflow1",
+ "wf",
+ [{ id: "op1", outputType: "csv" }],
+ 3,
+ 4,
+ "local-file",
+ expect.anything()
+ );
Review Comment:
Fixed — the local-export test now asserts outputType 'json' to match the
'json' input, and the determineOutputExtension stub echoes the requested export
type instead of a fixed 'csv'.
##########
frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts:
##########
@@ -136,6 +140,287 @@ describe("WorkflowResultExportService", () => {
expect(service.hasResultToExportOnHighlightedOperators).toBe(false);
expect(service.hasResultToExportOnAllOperators.value).toBe(false);
});
+
+ // ---- Test helpers
----------------------------------------------------------
+
+ function enableExport(): void {
+ const config = TestBed.inject(GuiConfigService) as unknown as
MockGuiConfigService;
+ config.setConfig({ exportExecutionResultEnabled: true });
+ }
Review Comment:
Fixed — beforeEach now resets GuiConfig to a known baseline
(exportExecutionResultEnabled: false) so each test starts disabled and
enableExport() can't leak state across execution order.
##########
frontend/src/app/workspace/service/workflow-result-export/workflow-result-export.service.spec.ts:
##########
@@ -136,6 +140,287 @@ describe("WorkflowResultExportService", () => {
expect(service.hasResultToExportOnHighlightedOperators).toBe(false);
expect(service.hasResultToExportOnAllOperators.value).toBe(false);
});
+
+ // ---- Test helpers
----------------------------------------------------------
+
+ function enableExport(): void {
+ const config = TestBed.inject(GuiConfigService) as unknown as
MockGuiConfigService;
+ config.setConfig({ exportExecutionResultEnabled: true });
+ }
+
+ function makeUnit(): DashboardWorkflowComputingUnit {
+ return { computingUnit: { cuid: 1, type: "local" } } as unknown as
DashboardWorkflowComputingUnit;
+ }
+
+ /** Stubs the export-flow methods the real DownloadService exposes but the
base spy omits. */
+ function stubDownloadService(overrides?: {
+ downloadability?: Record<string, string[]>;
+ datasetResponse?: HttpResponse<unknown>;
+ datasetError?: unknown;
+ }): {
+ getWorkflowResultDownloadability: ReturnType<typeof vi.fn>;
+ exportWorkflowResultToDataset: ReturnType<typeof vi.fn>;
+ exportWorkflowResultToLocal: ReturnType<typeof vi.fn>;
+ } {
+ const getWorkflowResultDownloadability =
vi.fn().mockReturnValue(of(overrides?.downloadability ?? {}));
+ const exportWorkflowResultToDataset = vi
+ .fn()
+ .mockReturnValue(
+ overrides?.datasetError !== undefined
+ ? throwError(() => overrides.datasetError)
+ : of(overrides?.datasetResponse ?? new HttpResponse({ body: {
status: "success", message: "ok" } }))
+ );
+ const exportWorkflowResultToLocal = vi.fn();
+ (downloadServiceSpy as any).getWorkflowResultDownloadability =
getWorkflowResultDownloadability;
+ (downloadServiceSpy as any).exportWorkflowResultToDataset =
exportWorkflowResultToDataset;
+ (downloadServiceSpy as any).exportWorkflowResultToLocal =
exportWorkflowResultToLocal;
+ (workflowResultServiceSpy as any).determineOutputExtension =
vi.fn().mockReturnValue("csv");
+ return { getWorkflowResultDownloadability, exportWorkflowResultToDataset,
exportWorkflowResultToLocal };
+ }
Review Comment:
Fixed — the three DownloadService methods are now defined on the typed
Mocked<DownloadService> spy at provider-creation time and configured via
.mockReturnValue, instead of patching through (spy as any).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]