This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-6435-4eb3673851cd84e87f85d73acd0b384678aa22ab in repository https://gitbox.apache.org/repos/asf/texera.git
commit 297e550068a9f06811b994cde2755e5d98e4a7fd Author: Prateek Ganigi <[email protected]> AuthorDate: Tue Jul 14 18:45:26 2026 -0700 test(frontend): add unit tests for computing-unit.util resource helpers (#6435) ### What changes were proposed in this PR? Adds unit test coverage for the pure resource-helper functions in [frontend/src/app/common/util/computing-unit.util.ts](vscode-webview://1epki5h79lmkghv36u4evg54fuvmk17ndjca203mcg7opnnd5sg9/frontend/src/app/common/util/computing-unit.util.ts), which previously had no direct test coverage. These functions parse Kubernetes resource strings, compute CPU/memory usage percentages, and derive UI status indicators. A new spec file, frontend/src/app/common/util/computing-unit.util.spec.ts, was created following the style of size-formatter.util.spec.ts (same directory). It adds 34 tests covering all 15 exported functions: parseResourceUnit / parseResourceNumber - unit extraction, decimals, and empty/NaN/invalid inputs cpuResourceConversion / memoryResourceConversion - precision scaling per target unit cpuPercentage / memoryPercentage - N/A and non-positive-limit guards, and the 100% clamp validateName - empty names, the 128-character boundary, and valid names getComputingUnitBadgeColor / getComputingUnitStatusTooltip - status mapping with fallback getComputingUnitCpuStatus / getComputingUnitMemoryStatus - percentage thresholds getComputingUnitCpuLimitUnit, isComputingUnitShmTooLarge (including mixed Mi/Gi comparisons), getJvmMemorySliderConfig, and buildLocalComputingUnitUri No production code was modified. Note: the issue lists a findNearestValidStep function that does not exist in the current codebase, so it was omitted; the actual exported functions are covered. ### Any related issues, documentation, discussions? Closes #6253 ### How was this PR tested? Added 34 unit tests in the new spec file, all passing via ng test: cd frontend && npx ng test --watch=false --include='**/computing-unit.util.spec.ts' ✓ src/app/common/util/computing-unit.util.spec.ts (34 tests) 10ms Test Files - 1, passed (1) Tests - 34, passed (34) ### Was this PR authored or co-authored using generative AI tooling? Co-authored with Claude Opus 4.8 in compliance with ASF --------- Signed-off-by: Xinyuan Lin <[email protected]> Co-authored-by: Xinyuan Lin <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> --- .../app/common/util/computing-unit.util.spec.ts | 241 +++++++++++++++++++++ 1 file changed, 241 insertions(+) diff --git a/frontend/src/app/common/util/computing-unit.util.spec.ts b/frontend/src/app/common/util/computing-unit.util.spec.ts new file mode 100644 index 0000000000..bbd4b57f1c --- /dev/null +++ b/frontend/src/app/common/util/computing-unit.util.spec.ts @@ -0,0 +1,241 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { DashboardWorkflowComputingUnit } from "../type/workflow-computing-unit"; +import { + parseResourceUnit, + parseResourceNumber, + cpuResourceConversion, + memoryResourceConversion, + cpuPercentage, + memoryPercentage, + validateName, + getComputingUnitBadgeColor, + getComputingUnitStatusTooltip, + getComputingUnitCpuStatus, + getComputingUnitMemoryStatus, + getComputingUnitCpuLimitUnit, + isComputingUnitShmTooLarge, + getJvmMemorySliderConfig, + buildLocalComputingUnitUri, +} from "./computing-unit.util"; + +describe("parseResourceUnit", () => { + it("should extract the unit from a resource string", () => { + expect(parseResourceUnit("500Mi")).toBe("Mi"); + expect(parseResourceUnit("2Gi")).toBe("Gi"); + expect(parseResourceUnit("1000m")).toBe("m"); + }); + + it("should return an empty string for a unitless number", () => { + expect(parseResourceUnit("2")).toBe(""); + expect(parseResourceUnit("1.5")).toBe(""); + }); + + it('should return "NaN" for empty or "NaN" input', () => { + expect(parseResourceUnit("")).toBe("NaN"); + expect(parseResourceUnit("NaN")).toBe("NaN"); + }); + + it("should return an empty string for a value that does not match the pattern", () => { + expect(parseResourceUnit("abc")).toBe(""); + }); +}); + +describe("parseResourceNumber", () => { + it("should extract the numeric value from a resource string", () => { + expect(parseResourceNumber("2Gi")).toBe(2); + expect(parseResourceNumber("500Mi")).toBe(500); + }); + + it("should handle decimal values", () => { + expect(parseResourceNumber("1.5Gi")).toBe(1.5); + }); + + it("should return 0 for empty, invalid, or NaN input", () => { + expect(parseResourceNumber("")).toBe(0); + expect(parseResourceNumber("NaN")).toBe(0); + expect(parseResourceNumber("abc")).toBe(0); + }); +}); + +describe("cpuResourceConversion", () => { + it("should convert millicores to cores with 4 decimals", () => { + expect(cpuResourceConversion("1000m", "")).toBe("1.0000"); + expect(cpuResourceConversion("500m", "")).toBe("0.5000"); + }); + + it("should convert cores to millicores with 2 decimals", () => { + expect(cpuResourceConversion("2", "m")).toBe("2000.00"); + }); + + it("should round to whole numbers for smaller units", () => { + expect(cpuResourceConversion("1", "u")).toBe("1000000"); + }); +}); + +describe("memoryResourceConversion", () => { + it("should convert between memory units with 4 decimals", () => { + expect(memoryResourceConversion("1Gi", "Gi")).toBe("1.0000"); + expect(memoryResourceConversion("1024Mi", "Gi")).toBe("1.0000"); + expect(memoryResourceConversion("512Mi", "Gi")).toBe("0.5000"); + }); +}); + +describe("cpuPercentage", () => { + it('should return 0 when usage or limit is "N/A"', () => { + expect(cpuPercentage("N/A", "1000m")).toBe(0); + expect(cpuPercentage("500m", "N/A")).toBe(0); + }); + + it("should return 0 when the limit is not positive", () => { + expect(cpuPercentage("500m", "0")).toBe(0); + }); + + it("should compute the usage percentage", () => { + expect(cpuPercentage("500m", "1000m")).toBe(50); + }); + + it("should clamp the percentage at 100", () => { + expect(cpuPercentage("2000m", "1000m")).toBe(100); + }); +}); + +describe("memoryPercentage", () => { + it('should return 0 when usage or limit is "N/A"', () => { + expect(memoryPercentage("N/A", "1Gi")).toBe(0); + expect(memoryPercentage("512Mi", "N/A")).toBe(0); + }); + + it("should compute the usage percentage", () => { + expect(memoryPercentage("512Mi", "1Gi")).toBe(50); + }); + + it("should clamp the percentage at 100", () => { + expect(memoryPercentage("2Gi", "1Gi")).toBe(100); + }); +}); + +describe("validateName", () => { + it("should return an error for an empty name", () => { + expect(validateName("")).toBe("Computing unit name cannot be empty"); + }); + + it("should return an error for names longer than 128 characters", () => { + expect(validateName("a".repeat(129))).toBe("Computing unit name cannot exceed 128 characters"); + }); + + it("should return null for valid names", () => { + expect(validateName("my-unit")).toBeNull(); + expect(validateName("a".repeat(128))).toBeNull(); + }); +}); + +describe("getComputingUnitBadgeColor", () => { + it("should map known statuses to colors", () => { + expect(getComputingUnitBadgeColor("Running")).toBe("green"); + expect(getComputingUnitBadgeColor("Pending")).toBe("gold"); + }); + + it("should default to red for unknown statuses", () => { + expect(getComputingUnitBadgeColor("Terminated")).toBe("red"); + }); +}); + +describe("getComputingUnitStatusTooltip", () => { + it("should map statuses to tooltip text", () => { + expect(getComputingUnitStatusTooltip({ status: "Running" } as unknown as DashboardWorkflowComputingUnit)).toBe( + "Ready to use" + ); + expect(getComputingUnitStatusTooltip({ status: "Pending" } as unknown as DashboardWorkflowComputingUnit)).toBe( + "Computing unit is starting up" + ); + }); + + it("should fall back to the raw status for unknown statuses", () => { + expect(getComputingUnitStatusTooltip({ status: "Terminated" } as unknown as DashboardWorkflowComputingUnit)).toBe( + "Terminated" + ); + }); +}); + +describe("getComputingUnitCpuStatus / getComputingUnitMemoryStatus", () => { + it("should derive status from the usage percentage", () => { + expect(getComputingUnitCpuStatus(95)).toBe("exception"); + expect(getComputingUnitCpuStatus(60)).toBe("normal"); + expect(getComputingUnitCpuStatus(30)).toBe("success"); + + expect(getComputingUnitMemoryStatus(95)).toBe("exception"); + expect(getComputingUnitMemoryStatus(60)).toBe("normal"); + expect(getComputingUnitMemoryStatus(30)).toBe("success"); + }); +}); + +describe("getComputingUnitCpuLimitUnit", () => { + it('should return "CPU" for an empty unit and the unit otherwise', () => { + expect(getComputingUnitCpuLimitUnit("")).toBe("CPU"); + expect(getComputingUnitCpuLimitUnit("m")).toBe("m"); + }); +}); + +describe("isComputingUnitShmTooLarge", () => { + it("should return true only when shared memory exceeds the selected memory", () => { + expect(isComputingUnitShmTooLarge("2Gi", 3, "Gi")).toBe(true); + expect(isComputingUnitShmTooLarge("2Gi", 1, "Gi")).toBe(false); + }); + + it("should return false when shared memory equals the selected memory", () => { + expect(isComputingUnitShmTooLarge("2Gi", 2, "Gi")).toBe(false); + }); + + it("should compare across mixed units", () => { + expect(isComputingUnitShmTooLarge("1Gi", 2048, "Mi")).toBe(true); + expect(isComputingUnitShmTooLarge("1Gi", 512, "Mi")).toBe(false); + }); +}); + +describe("getJvmMemorySliderConfig", () => { + it("should hide the slider for small memory sizes", () => { + const config = getJvmMemorySliderConfig("1Gi"); + expect(config.showJvmMemorySlider).toBe(false); + expect(config.jvmMemorySteps).toEqual([1]); + expect(config.selectedJvmMemorySize).toBe("1G"); + }); + + it("should show the slider and build doubling steps for larger memory sizes", () => { + const config = getJvmMemorySliderConfig("8Gi"); + expect(config.showJvmMemorySlider).toBe(true); + expect(config.jvmMemorySteps).toEqual([2, 4, 8]); + expect(config.selectedJvmMemorySize).toBe("2G"); + }); +}); + +describe("buildLocalComputingUnitUri", () => { + it("should build a uri including the port when present", () => { + expect(buildLocalComputingUnitUri({ protocol: "http:", hostname: "localhost", port: "8080" })).toBe( + "http://localhost:8080/wsapi" + ); + }); + + it("should omit the port when it is empty", () => { + expect(buildLocalComputingUnitUri({ protocol: "https:", hostname: "example.com", port: "" })).toBe( + "https://example.com/wsapi" + ); + }); +});
