Skip to content

Commit fe77161

Browse files
authored
refactor(e2e): match exclude globs with Bun.Glob in the oracle (#563)
1 parent b2ed711 commit fe77161

2 files changed

Lines changed: 48 additions & 42 deletions

File tree

‎test/e2e/generators/oracle.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,24 @@ describe("predictDiscovery filter rules", () => {
11621162
{ exclude: "uPPer" },
11631163
["e2e-owner/svc-a", "e2e-owner/svc-b", "e2e-owner/legacy-x"],
11641164
],
1165+
[
1166+
"exclude globs: a character class reads as the action's matcher reads it, not literally",
1167+
globPool,
1168+
{ exclude: "svc-[ab]" },
1169+
["e2e-owner/legacy-x", "e2e-owner/UPPER"],
1170+
],
1171+
[
1172+
"exclude globs: a class range spanning the cases admits a letter of either case and not the gap between them",
1173+
[{ slug: "e2e-owner/svc-_" }, { slug: "e2e-owner/svc-a" }, { slug: "e2e-owner/svc-Q" }],
1174+
{ exclude: "svc-[0-Z]" },
1175+
["e2e-owner/svc-_"],
1176+
],
1177+
[
1178+
'exclude globs: a leading "!" negates, so only the matches stay',
1179+
globPool,
1180+
{ exclude: "!svc-*" },
1181+
["e2e-owner/svc-a", "e2e-owner/svc-b"],
1182+
],
11651183
])("%s", (_label, repos, filters, kept) => {
11661184
expect(predictDiscovery(repos, filters)).toEqual(kept);
11671185
});

‎test/e2e/generators/oracle.ts‎

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ const MULTI_RESULT_ORDER = ["failed", "drift", "partial", "skipped", "applied",
8989
/** The visibility filter values that narrow the pool; any other value keeps every repo, as the action does. */
9090
const VISIBILITY_FILTERS: ReadonlySet<string> = new Set(["public", "private", "internal"]);
9191

92+
/** Every character an owner or repository name holds, "-" first so a rewritten class reads it as a literal. */
93+
const NAME_CHARACTERS = "-._0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
94+
95+
/** A glob class, its opener and members apart, or one letter outside a class. */
96+
const CLASS_OR_LETTER = /\[([!^]?)([^\]]*)\]|[a-z]/gi;
97+
9298
/** Verbatim: the fold for every key GitHub matches exactly. */
9399
const same = (name: string): string => name;
94100

@@ -736,53 +742,35 @@ export function predictMulti(meta: MultiScenarioMeta): MultiPrediction {
736742
return { repos, allowedExitCodes: multiExitCodes(repos), forbidden };
737743
}
738744

739-
/** A pattern with "/" matches the full slug, otherwise the name portion, mirroring the repos-dir <name>.yml vs <owner>/<name>.yml split. */
740-
function globTarget(pattern: string, slug: string): string {
741-
return (pattern.includes("/") ? slug : (slug.split("/")[1] ?? slug)).toLowerCase();
742-
}
743-
744-
/** Whether one pattern unit takes the target character under it: a literal match, or a wildcard. */
745-
function globUnitMatches(unit: string | undefined, char: string | undefined): boolean {
746-
return unit !== undefined && (unit === char || unit === "*" || unit === "?");
745+
/**
746+
* GitHub compares names case-insensitively and Bun.Glob has no mode for it, so the pattern is widened to admit
747+
* either case the way the action's nocase regex does: a letter outside a class becomes "[xX]", and a class
748+
* becomes the name characters it admits in either case. Folding the target instead would misread a range that
749+
* spans the cases: lowercased, "[0-Z]" is "[0-z]" and gains "_".
750+
*/
751+
function eitherCase(pattern: string): string {
752+
return pattern.replace(CLASS_OR_LETTER, (unit, opener: string, members: string | undefined) => {
753+
if (members === undefined) {
754+
return `[${unit.toLowerCase()}${unit.toUpperCase()}]`;
755+
}
756+
const admits = new Bun.Glob(`[${members}]`);
757+
const twin = (char: string): string =>
758+
char === char.toLowerCase() ? char.toUpperCase() : char.toLowerCase();
759+
const admitted = [...NAME_CHARACTERS].filter(
760+
(char) => admits.match(char) || admits.match(twin(char)),
761+
);
762+
return `[${opener}${admitted.join("")}]`;
763+
});
747764
}
748765

749766
/**
750-
* An INDEPENDENT glob matcher, deliberately not src's compileExcludePattern (a picomatch compile), so a bug in
751-
* either surfaces as a disagreement; it covers the forms the scenarios write ("*", "?", a leading "!"), not
752-
* the whole picomatch grammar.
767+
* The runtime's glob, not src's compileExcludePattern (a picomatch compile), so a bug in either surfaces as a
768+
* disagreement. A pattern with "/" matches the full slug, otherwise the name portion, mirroring the repos-dir
769+
* <name>.yml vs <owner>/<name>.yml split.
753770
*/
754771
function globMatches(pattern: string, slug: string): boolean {
755-
if (pattern.startsWith("!")) {
756-
return !globMatches(pattern.slice(1), slug);
757-
}
758-
const target = globTarget(pattern, slug);
759-
const pat = pattern.toLowerCase();
760-
let p = 0;
761-
let t = 0;
762-
let star = -1;
763-
let mark = 0;
764-
while (t < target.length) {
765-
if (globUnitMatches(pat[p], target[t])) {
766-
if (pat[p] === "*") {
767-
star = p;
768-
mark = t;
769-
p++;
770-
} else {
771-
p++;
772-
t++;
773-
}
774-
} else if (star === -1) {
775-
return false;
776-
} else {
777-
p = star + 1;
778-
mark++;
779-
t = mark;
780-
}
781-
}
782-
while (p < pat.length && pat[p] === "*") {
783-
p++;
784-
}
785-
return p === pat.length;
772+
const target = pattern.includes("/") ? slug : (slug.split("/")[1] ?? slug);
773+
return new Bun.Glob(eitherCase(pattern)).match(target);
786774
}
787775

788776
/** The action's documented filter rules over one pool repo, in the engine's attribution order. */

0 commit comments

Comments
 (0)