Skip to content
Merged
4 changes: 1 addition & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: Build

on:
pull_request:
types:
- opened
push:
branches:
- main
Expand All @@ -14,7 +12,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Install dependencies
run: npm install
Expand Down
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = function (api) {
regenerator: false,
useESModules: false, // don't output es-modules by default
corejs: false,
helpers: false,
helpers: true,
},
];

Expand Down
157 changes: 157 additions & 0 deletions src/__tests__/jsonexpr/evaluator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,161 @@ describe("Evaluator", () => {
expect(evaluator.compare("100", "9")).toBe(-1);
});
});

describe("versionCompare()", () => {
it("should return 0 for equal versions", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.0.0", "1.0.0")).toBe(0);
expect(evaluator.versionCompare("0.0.0", "0.0.0")).toBe(0);
expect(evaluator.versionCompare("999.999.999", "999.999.999")).toBe(0);
});

it("should compare major versions", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("2.0.0", "1.0.0")).toBe(1);
expect(evaluator.versionCompare("1.0.0", "2.0.0")).toBe(-1);
});

it("should compare minor versions", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.2.0", "1.1.0")).toBe(1);
expect(evaluator.versionCompare("1.1.0", "1.2.0")).toBe(-1);
});

it("should compare patch versions", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.0.2", "1.0.1")).toBe(1);
expect(evaluator.versionCompare("1.0.1", "1.0.2")).toBe(-1);
});

it("should compare numerically not lexicographically", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.10.0", "1.9.0")).toBe(1);
expect(evaluator.versionCompare("1.9.0", "1.10.0")).toBe(-1);
expect(evaluator.versionCompare("10.0.0", "9.0.0")).toBe(1);
});

it("should treat missing parts as 0", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.2", "1.2.0")).toBe(0);
expect(evaluator.versionCompare("1", "1.0.0")).toBe(0);
expect(evaluator.versionCompare("1.2.0", "1.2")).toBe(0);
expect(evaluator.versionCompare("1.0.0", "1")).toBe(0);
});

it("should handle leading zeros in version parts", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.02.0", "1.2.0")).toBe(0);
expect(evaluator.versionCompare("1.002.030", "1.2.30")).toBe(0);
expect(evaluator.versionCompare("01.0.0", "1.0.0")).toBe(0);
expect(evaluator.versionCompare("1.02.0", "1.3.0")).toBe(-1);
});

it("should handle pre-release versions", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.0.0", "1.0.0-alpha")).toBe(1);
expect(evaluator.versionCompare("1.0.0-alpha", "1.0.0")).toBe(-1);
expect(evaluator.versionCompare("1.0.0-alpha", "1.0.0-alpha")).toBe(0);
expect(evaluator.versionCompare("1.0.0-alpha", "1.0.0-beta")).toBe(-1);
expect(evaluator.versionCompare("1.0.0-beta", "1.0.0-alpha")).toBe(1);
expect(evaluator.versionCompare("1.0.0-alpha.1", "1.0.0-alpha.2")).toBe(-1);
expect(evaluator.versionCompare("1.0.0-alpha.2", "1.0.0-alpha.1")).toBe(1);
expect(evaluator.versionCompare("1.0.0-1", "1.0.0-2")).toBe(-1);
expect(evaluator.versionCompare("1.0.0-2", "1.0.0-1")).toBe(1);
});

it("should compare numeric pre-release identifiers as less than string identifiers", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.0.0-1", "1.0.0-alpha")).toBe(-1);
expect(evaluator.versionCompare("1.0.0-alpha", "1.0.0-1")).toBe(1);
});

it("should compare pre-release with fewer identifiers as less", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.0.0-alpha", "1.0.0-alpha.1")).toBe(-1);
expect(evaluator.versionCompare("1.0.0-alpha.1", "1.0.0-alpha")).toBe(1);
});

it("should strip v prefix", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("v1.0.0", "1.0.0")).toBe(0);
expect(evaluator.versionCompare("V1.0.0", "1.0.0")).toBe(0);
expect(evaluator.versionCompare("v1.0.0", "V1.0.0")).toBe(0);
});

it("should return null for null inputs", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare(null, "1.0.0")).toBe(null);
expect(evaluator.versionCompare("1.0.0", null)).toBe(null);
expect(evaluator.versionCompare(null, null)).toBe(null);
});

it("should coerce non-string inputs via stringConvert", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare(1, "1.0.0")).toBe(0);
expect(evaluator.versionCompare("1.0.0", 1)).toBe(0);
expect(evaluator.versionCompare(true, "true")).toBe(0);
});

it("should return null for unconvertible inputs", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare({}, "1.0.0")).toBe(null);
expect(evaluator.versionCompare("1.0.0", {})).toBe(null);
expect(evaluator.versionCompare([], "1.0.0")).toBe(null);
});

it("should ignore build metadata", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.0.0+build1", "1.0.0+build2")).toBe(0);
expect(evaluator.versionCompare("1.0.0+build1", "1.0.0")).toBe(0);
});

it("should handle pre-release combined with build metadata", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("1.0.0-alpha+build1", "1.0.0-alpha+build2")).toBe(0);
expect(evaluator.versionCompare("1.0.0-alpha+build1", "1.0.0-beta")).toBe(-1);
expect(evaluator.versionCompare("1.0.0-alpha+build1", "1.0.0")).toBe(-1);
});

it("should return null for empty string inputs", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("", "1.0.0")).toBe(null);
expect(evaluator.versionCompare("1.0.0", "")).toBe(null);
expect(evaluator.versionCompare("", "")).toBe(null);
});

it("should return null for undefined inputs", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare(undefined, "1.0.0")).toBe(null);
expect(evaluator.versionCompare("1.0.0", undefined)).toBe(null);
});

it("should return null for inputs that normalize to empty core", () => {
const evaluator = new Evaluator({}, {});

expect(evaluator.versionCompare("v", "1.0.0")).toBe(null);
expect(evaluator.versionCompare("V", "1.0.0")).toBe(null);
expect(evaluator.versionCompare("+build", "1.0.0")).toBe(null);
expect(evaluator.versionCompare("v+build", "1.0.0")).toBe(null);
expect(evaluator.versionCompare("1.0.0", "v")).toBe(null);
});
});
});
15 changes: 15 additions & 0 deletions src/__tests__/jsonexpr/operators/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ export function mockEvaluator() {
return expr;
}),

versionCompare: jest.fn((lhs, rhs) => {
const lhsStr = typeof lhs === "string" ? lhs : null;
const rhsStr = typeof rhs === "string" ? rhs : null;
if (lhsStr === null || rhsStr === null) return null;
const lParts = lhsStr.split(".").map(Number);
const rParts = rhsStr.split(".").map(Number);
const len = Math.max(lParts.length, rParts.length);
for (let i = 0; i < len; i++) {
const l = lParts[i] || 0;
const r = rParts[i] || 0;
if (l !== r) return l > r ? 1 : -1;
}
return 0;
}),

compare: jest.fn((lhs, rhs) => {
switch (typeof lhs) {
case "boolean":
Expand Down
38 changes: 38 additions & 0 deletions src/__tests__/jsonexpr/operators/semver_eq.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { mockEvaluator } from "./evaluator";
import { SemverEqualsOperator } from "../../../jsonexpr/operators/semver_eq";

describe("SemverEqualsOperator", () => {
const operator = new SemverEqualsOperator();

describe("evaluate", () => {
const evaluator = mockEvaluator();

afterEach(() => {
evaluator.evaluate.mockClear();
evaluator.versionCompare.mockClear();
});

it("should return true when versions are equal", () => {
expect(operator.evaluate(evaluator, ["1.0.0", "1.0.0"])).toBe(true);
expect(evaluator.evaluate).toHaveBeenCalledTimes(2);
expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "1.0.0");
});

it("should return false when versions are not equal", () => {
expect(operator.evaluate(evaluator, ["1.0.0", "2.0.0"])).toBe(false);
expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "2.0.0");
});

it("should return null when lhs is null", () => {
expect(operator.evaluate(evaluator, [null, "1.0.0"])).toBe(null);
expect(evaluator.evaluate).toHaveBeenCalledTimes(1);
expect(evaluator.versionCompare).not.toHaveBeenCalled();
});

it("should return null when rhs is null", () => {
expect(operator.evaluate(evaluator, ["1.0.0", null])).toBe(null);
expect(evaluator.evaluate).toHaveBeenCalledTimes(2);
expect(evaluator.versionCompare).not.toHaveBeenCalled();
});
});
});
42 changes: 42 additions & 0 deletions src/__tests__/jsonexpr/operators/semver_gt.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mockEvaluator } from "./evaluator";
import { SemverGreaterThanOperator } from "../../../jsonexpr/operators/semver_gt";

describe("SemverGreaterThanOperator", () => {
const operator = new SemverGreaterThanOperator();

describe("evaluate", () => {
const evaluator = mockEvaluator();

afterEach(() => {
evaluator.evaluate.mockClear();
evaluator.versionCompare.mockClear();
});

it("should return true when left version is greater", () => {
expect(operator.evaluate(evaluator, ["2.0.0", "1.0.0"])).toBe(true);
expect(evaluator.versionCompare).toHaveBeenCalledWith("2.0.0", "1.0.0");
});

it("should return false when versions are equal", () => {
expect(operator.evaluate(evaluator, ["1.0.0", "1.0.0"])).toBe(false);
expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "1.0.0");
});

it("should return false when left version is less", () => {
expect(operator.evaluate(evaluator, ["1.0.0", "2.0.0"])).toBe(false);
expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "2.0.0");
});

it("should return null when lhs is null", () => {
expect(operator.evaluate(evaluator, [null, "1.0.0"])).toBe(null);
expect(evaluator.evaluate).toHaveBeenCalledTimes(1);
expect(evaluator.versionCompare).not.toHaveBeenCalled();
});

it("should return null when rhs is null", () => {
expect(operator.evaluate(evaluator, ["1.0.0", null])).toBe(null);
expect(evaluator.evaluate).toHaveBeenCalledTimes(2);
expect(evaluator.versionCompare).not.toHaveBeenCalled();
});
});
});
42 changes: 42 additions & 0 deletions src/__tests__/jsonexpr/operators/semver_gte.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mockEvaluator } from "./evaluator";
import { SemverGreaterThanOrEqualOperator } from "../../../jsonexpr/operators/semver_gte";

describe("SemverGreaterThanOrEqualOperator", () => {
const operator = new SemverGreaterThanOrEqualOperator();

describe("evaluate", () => {
const evaluator = mockEvaluator();

afterEach(() => {
evaluator.evaluate.mockClear();
evaluator.versionCompare.mockClear();
});

it("should return true when left version is greater", () => {
expect(operator.evaluate(evaluator, ["2.0.0", "1.0.0"])).toBe(true);
expect(evaluator.versionCompare).toHaveBeenCalledWith("2.0.0", "1.0.0");
});

it("should return true when versions are equal", () => {
expect(operator.evaluate(evaluator, ["1.0.0", "1.0.0"])).toBe(true);
expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "1.0.0");
});

it("should return false when left version is less", () => {
expect(operator.evaluate(evaluator, ["1.0.0", "2.0.0"])).toBe(false);
expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "2.0.0");
});

it("should return null when lhs is null", () => {
expect(operator.evaluate(evaluator, [null, "1.0.0"])).toBe(null);
expect(evaluator.evaluate).toHaveBeenCalledTimes(1);
expect(evaluator.versionCompare).not.toHaveBeenCalled();
});

it("should return null when rhs is null", () => {
expect(operator.evaluate(evaluator, ["1.0.0", null])).toBe(null);
expect(evaluator.evaluate).toHaveBeenCalledTimes(2);
expect(evaluator.versionCompare).not.toHaveBeenCalled();
});
});
});
42 changes: 42 additions & 0 deletions src/__tests__/jsonexpr/operators/semver_lt.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mockEvaluator } from "./evaluator";
import { SemverLessThanOperator } from "../../../jsonexpr/operators/semver_lt";

describe("SemverLessThanOperator", () => {
const operator = new SemverLessThanOperator();

describe("evaluate", () => {
const evaluator = mockEvaluator();

afterEach(() => {
evaluator.evaluate.mockClear();
evaluator.versionCompare.mockClear();
});

it("should return true when left version is less", () => {
expect(operator.evaluate(evaluator, ["1.0.0", "2.0.0"])).toBe(true);
expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "2.0.0");
});

it("should return false when versions are equal", () => {
expect(operator.evaluate(evaluator, ["1.0.0", "1.0.0"])).toBe(false);
expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "1.0.0");
});

it("should return false when left version is greater", () => {
expect(operator.evaluate(evaluator, ["2.0.0", "1.0.0"])).toBe(false);
expect(evaluator.versionCompare).toHaveBeenCalledWith("2.0.0", "1.0.0");
});

it("should return null when lhs is null", () => {
expect(operator.evaluate(evaluator, [null, "1.0.0"])).toBe(null);
expect(evaluator.evaluate).toHaveBeenCalledTimes(1);
expect(evaluator.versionCompare).not.toHaveBeenCalled();
});

it("should return null when rhs is null", () => {
expect(operator.evaluate(evaluator, ["1.0.0", null])).toBe(null);
expect(evaluator.evaluate).toHaveBeenCalledTimes(2);
expect(evaluator.versionCompare).not.toHaveBeenCalled();
});
});
});
Loading
Loading