Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(semver): add missing tests #6362

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion semver/compare_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { assertEquals } from "@std/assert";
import { assertEquals, assertThrows } from "@std/assert";
import { parse } from "./parse.ts";
import { compare } from "./compare.ts";

Expand Down Expand Up @@ -54,3 +54,50 @@ Deno.test({
}
},
});

Deno.test({
name: "compare() throws on NaN",
fn: () => {
assertThrows(
() =>
compare({ major: NaN, minor: 0, patch: 0 }, {
major: 1,
minor: 0,
patch: 0,
}),
Error,
"Cannot compare against non-numbers",
);

assertThrows(
() =>
compare({ major: 1, minor: 0, patch: 0 }, {
major: 1,
minor: NaN,
patch: 0,
}),
Error,
"Cannot compare against non-numbers",
);
},
});

Deno.test({
name: "compare() handles undefined in prerelease",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case looks comparing invalid semvers. I don't think this is an intentional behavior, but an undefined behavior. I'd suggest we should remove this test case.

fn: () => {
assertEquals(
compare({
major: 1,
minor: 0,
patch: 0,
prerelease: [undefined as unknown as string],
}, {
major: 1,
minor: 0,
patch: 0,
prerelease: [undefined as unknown as string],
}),
0,
);
},
});
43 changes: 42 additions & 1 deletion semver/greater_than_range_test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// Copyright Isaac Z. Schlueter and npm contributors. All rights reserved. ISC license.

import { assert, assertFalse } from "@std/assert";
import { assert, assertEquals, assertFalse } from "@std/assert";
import {
format,
formatRange,
greaterThanRange,
parse,
parseRange,
} from "./mod.ts";
import type { Operator } from "./types.ts";

Deno.test("greaterThanRange() checks if the semver is greater than the range", async (t) => {
// from https://github.com/npm/node-semver/blob/692451bd6f75b38a71a99f39da405c94a5954a22/test/fixtures/version-gt-range.js
Expand Down Expand Up @@ -167,3 +168,43 @@ Deno.test("greaterThanRange() checks if the semver is greater than the range", a
});
}
});

Deno.test("greaterThanRange() handles equals operator", () => {
const version = {
major: 1,
minor: 0,
patch: 0,
prerelease: [],
build: [],
};
const range = [[{
operator: "=" as unknown as Operator,
major: 1,
minor: 0,
patch: 0,
prerelease: [],
build: [],
}]];
assertEquals(greaterThanRange(version, range), false);
});

// BUG `!=` operator type does not exist in semver and should be removed.
// TODO remove this test once `!=` operator is removed.
Deno.test("greaterThanRange() handles not equals operator", () => {
const version = {
major: 1,
minor: 0,
patch: 0,
prerelease: [],
build: [],
};
const range = [[{
operator: "!=" as unknown as Operator,
major: 1,
minor: 0,
patch: 0,
prerelease: [],
build: [],
}]];
assertEquals(greaterThanRange(version, range), true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this result is correct

I also just realized != is not a valid operator in npm:semver (semver.gtr("1.0.0", "!=1.0.0") throws with invalid operator error, and docs doesn't mention != https://github.com/npm/node-semver?tab=readme-ov-file#ranges

I'd suggest we should skip this particular test case for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of skipping, we probably should add the test and remove it when handling the problem/bug.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then can you add a TODO comment above the test title, which says that this is a bug, not an intentional behavior?

});
66 changes: 52 additions & 14 deletions semver/is_range_test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { assert } from "@std/assert";
import { assertEquals } from "@std/assert";
import { ALL, MIN } from "./_constants.ts";
import { formatRange } from "./format_range.ts";
import { isRange } from "./is_range.ts";
import type { Range } from "./types.ts";

Deno.test({
name: "isRange()",
fn: async (t) => {
const ranges: Range[] = [[
[ALL],
], [
name: "isRange() handles simple range",
fn: () => {
const range: Range = [
[{
operator: ">=",
major: 0,
Expand All @@ -22,12 +19,53 @@ Deno.test({
operator: "<",
...MIN,
}],
]];
for (const r of ranges) {
await t.step(`${formatRange(r)}`, () => {
const actual = isRange(r);
assert(actual);
});
}
];
const actual = isRange(range);
assertEquals(actual, true);
},
});

Deno.test({
name: "isRange() handles ALL constant",
fn: () => {
const range: Range = [[ALL]];
const actual = isRange(range);
assertEquals(actual, true);
},
});

Deno.test({
name: "isRange() handles null",
fn: () => {
const range: Range = [[null]] as unknown as Range;
const actual = isRange(range);
assertEquals(actual, false);
},
});

Deno.test({
name: "isRange() handles undefined",
fn: () => {
const range: Range = [[undefined]] as unknown as Range;
const actual = isRange(range);
assertEquals(actual, false);
},
});

Deno.test({
name: "isRange() handles array type",
fn: () => {
const range: Range = [[[1, 2, 3]]] as unknown as Range;
const actual = isRange(range);
assertEquals(actual, false);
},
});

Deno.test({
name: "isRange() handles not object type",
fn: () => {
const range: Range = [[[true]]] as unknown as Range;
const actual = isRange(range);
assertEquals(actual, false);
},
});
3 changes: 2 additions & 1 deletion semver/is_semver_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { assert } from "@std/assert";
import { MIN } from "./_constants.ts";
import { ANY, MIN } from "./_constants.ts";
import { isSemVer } from "./is_semver.ts";

Deno.test({
Expand Down Expand Up @@ -61,6 +61,7 @@ Deno.test({
[{ major: 0, minor: 0, patch: 0, build: [], prerelease: ["abc"] }],
[{ major: 0, minor: 0, patch: 0, build: [], prerelease: ["abc", 0] }],
[MIN],
[ANY],
];
for (const [v] of versions) {
await t.step(
Expand Down
22 changes: 21 additions & 1 deletion semver/less_than_range_test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// Copyright Isaac Z. Schlueter and npm contributors. All rights reserved. ISC license.

import { assert, assertFalse } from "@std/assert";
import { assert, assertEquals, assertFalse } from "@std/assert";
import {
format,
formatRange,
lessThanRange,
type Operator,
parse,
parseRange,
} from "./mod.ts";
Expand Down Expand Up @@ -169,3 +170,22 @@ Deno.test("lessThanRange() checks if the SemVer is less than the range", async (
});
}
});

Deno.test("lessThanRange() handles not equals operator", () => {
const version = {
major: 1,
minor: 0,
patch: 0,
prerelease: [],
build: [],
};
const range = [[{
operator: "!=" as unknown as Operator,
major: 1,
minor: 0,
patch: 0,
prerelease: [],
build: [],
}]];
assertEquals(lessThanRange(version, range), true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This result feels wrong to me. I'd suggest skipping this test case for now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not skip test cases if they yield unexpected results but fix them instead. What would the proper result be?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the proper result be?

The range != 1.0.0 should include all versions except 1.0.0. So I think the expected result here is false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to create a PR to remove the != straight away as a bug fix before landing this PR? This probably solves lots of headaches with intermediate patches.

});
12 changes: 5 additions & 7 deletions semver/parse_range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,27 +141,25 @@ function handleRightHyphenRangeGroups(
major: +rightGroups.major,
minor: +rightGroups.minor,
patch: +rightGroups.patch,
prerelease: rightGroups.prerelease
? parsePrerelease(rightGroups.prerelease)
: [],
prerelease: [],
build: [],
};
}
function parseHyphenRange(range: string): Comparator[] | undefined {
function parseHyphenRange(range: string): Comparator[] | null {
const leftMatch = range.match(new RegExp(`^${XRANGE}`));
const leftGroup = leftMatch?.groups;
if (!leftGroup) return;
if (!leftGroup) return null;
const leftLength = leftMatch[0].length;

const hyphenMatch = range.slice(leftLength).match(/^\s+-\s+/);
if (!hyphenMatch) return;
if (!hyphenMatch) return null;
const hyphenLength = hyphenMatch[0].length;

const rightMatch = range.slice(leftLength + hyphenLength).match(
new RegExp(`^${XRANGE}\\s*$`),
);
const rightGroups = rightMatch?.groups;
if (!rightGroups) return;
if (!rightGroups) return null;

const from = handleLeftHyphenRangeGroups(leftGroup as RangeRegExpGroups);
const to = handleRightHyphenRangeGroups(rightGroups as RangeRegExpGroups);
Expand Down
21 changes: 21 additions & 0 deletions semver/parse_range_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,24 @@ Deno.test("parseRange() throws on invalid range", () => {
'Cannot parse version range: range "blerg" is invalid',
);
});

Deno.test("parseRange() handles wildcards", () => {
assertEquals(parseRange("<1.*"), [
[{ operator: "<", major: 1, minor: 0, patch: 0 }],
]);
assertEquals(parseRange("<1.*.0"), [
[{ operator: "<", major: 1, minor: NaN, patch: 0 }],
]);
Comment on lines +671 to +676
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference of these 2 results seem strange to me. Is this intentionally in this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems indeed wrong. Imo <1.*.0 should be an invalid range and throw.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertEquals(parseRange("<1.*.*"), [
[{ operator: "<", major: 1, minor: 0, patch: 0 }],
]);
assertEquals(parseRange(">=1.*.0"), [
[{ operator: ">=", major: 1, minor: NaN, patch: 0 }],
]);
assertEquals(parseRange(">=1.*.*"), [
[{ operator: ">=", major: 1, minor: 0, patch: 0 }],
]);
assertEquals(parseRange(">=1.0.*"), [
[{ operator: ">=", major: 1, minor: 0, patch: 0 }],
]);
});
34 changes: 34 additions & 0 deletions semver/try_parse_range_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { assertEquals } from "@std/assert";
import { tryParseRange } from "./try_parse_range.ts";
import type { Range } from "./types.ts";

Deno.test("tryParseRange()", () => {
const actual = tryParseRange(">=1.2.3 <1.2.4");
const expected: Range = [
[
{
operator: ">=",
major: 1,
minor: 2,
patch: 3,
prerelease: [],
build: [],
},
{
operator: "<",
major: 1,
minor: 2,
patch: 4,
prerelease: [],
build: [],
},
],
];
assertEquals(actual, expected);
});

Deno.test("tryParseRange() handles invalid range", () => {
assertEquals(tryParseRange("blerg"), undefined);
});
1 change: 1 addition & 0 deletions semver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ReleaseType =
export type Operator =
| undefined
| "="
// `!=` operator type does not exist in semver and should be removed.
| "!="
| ">"
| ">="
Expand Down
Loading