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]

Reply via email to