Skip to content

Commit 8f88c96

Browse files
committed
Move heavy benchmarks to .bench and clean tests
Rename benchmark.test.ts to benchmark.bench.ts and move the large benchmarking suite and helper builders (scaling, adversarial, realistic, edge cases) into the new .bench file. Trim binaryMeetingTime.test.ts to keep unit tests (remove the long benchmark helpers and brute-force generator/original comparisons), and update imports accordingly. Also remove the re-export of incrementIndex from generateSchedules.ts to avoid exposing that internal helper.
1 parent ab833ae commit 8f88c96

5 files changed

Lines changed: 845 additions & 707 deletions

File tree

apps/searchneu/lib/scheduler/binaryMeetingTimeTests/addOptionalCourses.test.ts

Lines changed: 157 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
generateCombinationsOptimized,
88
MAX_RESULTS,
99
} from "../generateCombinations";
10-
import { meetingTimesToBinaryMask, hasConflictInSchedule } from "../binaryMeetingTime";
10+
import {
11+
meetingTimesToBinaryMask,
12+
hasConflictInSchedule,
13+
} from "../binaryMeetingTime";
1114
import { createMockSection } from "./mocks";
1215

1316
// ---------------------------------------------------------------------------
@@ -38,19 +41,35 @@ function toIdSets(schedules: SectionWithCourse[][]): Set<number>[] {
3841
// Fixtures — non-conflicting time slots
3942
// ---------------------------------------------------------------------------
4043

41-
const S_MON_8 = createMockSection(1, [{ days: [1], startTime: 800, endTime: 900 }]);
42-
const S_MON_10 = createMockSection(2, [{ days: [1], startTime: 1000, endTime: 1100 }]);
43-
const S_MON_12 = createMockSection(3, [{ days: [1], startTime: 1200, endTime: 1300 }]);
44-
const S_TUE_8 = createMockSection(4, [{ days: [2], startTime: 800, endTime: 900 }]);
45-
const S_TUE_10 = createMockSection(5, [{ days: [2], startTime: 1000, endTime: 1100 }]);
46-
const S_WED_8 = createMockSection(6, [{ days: [3], startTime: 800, endTime: 900 }]);
47-
const S_WED_10 = createMockSection(7, [{ days: [3], startTime: 1000, endTime: 1100 }]);
44+
const S_MON_8 = createMockSection(1, [
45+
{ days: [1], startTime: 800, endTime: 900 },
46+
]);
47+
const S_MON_10 = createMockSection(2, [
48+
{ days: [1], startTime: 1000, endTime: 1100 },
49+
]);
50+
const S_MON_12 = createMockSection(3, [
51+
{ days: [1], startTime: 1200, endTime: 1300 },
52+
]);
53+
const S_TUE_8 = createMockSection(4, [
54+
{ days: [2], startTime: 800, endTime: 900 },
55+
]);
56+
const S_TUE_10 = createMockSection(5, [
57+
{ days: [2], startTime: 1000, endTime: 1100 },
58+
]);
59+
const S_WED_8 = createMockSection(6, [
60+
{ days: [3], startTime: 800, endTime: 900 },
61+
]);
62+
const S_WED_10 = createMockSection(7, [
63+
{ days: [3], startTime: 1000, endTime: 1100 },
64+
]);
4865
// Conflicts with S_MON_8 (same day/time)
49-
const S_MON_8_B = createMockSection(8, [{ days: [1], startTime: 800, endTime: 900 }]);
66+
const S_MON_8_B = createMockSection(8, [
67+
{ days: [1], startTime: 800, endTime: 900 },
68+
]);
5069
// Multi-meeting: MWF lecture + TR lab
5170
const S_MWF_LECTURE_TR_LAB = createMockSection(9, [
52-
{ days: [1, 3, 5], startTime: 900, endTime: 950 }, // MWF 9-9:50
53-
{ days: [2, 4], startTime: 1100, endTime: 1150 }, // TR 11-11:50
71+
{ days: [1, 3, 5], startTime: 900, endTime: 950 }, // MWF 9-9:50
72+
{ days: [2, 4], startTime: 1100, endTime: 1150 }, // TR 11-11:50
5473
]);
5574
// Conflicts with lecture block
5675
const S_MON_OVERLAP_LECTURE = createMockSection(10, [
@@ -90,7 +109,11 @@ describe("addOptionalCourses — basic correctness", () => {
90109
assert.equal(results.length, 2);
91110
const idSets = toIdSets(results);
92111
assert.ok(idSets.some((s) => s.size === 1 && s.has(S_MON_8.id)));
93-
assert.ok(idSets.some((s) => s.size === 2 && s.has(S_MON_8.id) && s.has(S_TUE_10.id)));
112+
assert.ok(
113+
idSets.some(
114+
(s) => s.size === 2 && s.has(S_MON_8.id) && s.has(S_TUE_10.id),
115+
),
116+
);
94117
});
95118

96119
test("one optional course, always conflicts → returns only base schedule", () => {
@@ -148,7 +171,10 @@ describe("addOptionalCourses — basic correctness", () => {
148171

149172
test("no output schedules contain time conflicts", () => {
150173
const base = [S_MON_8, S_TUE_8];
151-
const optionals = [[S_MON_10, S_MON_8_B], [S_WED_10, S_WED_8]];
174+
const optionals = [
175+
[S_MON_10, S_MON_8_B],
176+
[S_WED_10, S_WED_8],
177+
];
152178
const results = addOptionalCourses(
153179
base,
154180
combinedMask(base),
@@ -211,7 +237,7 @@ describe("addOptionalCourses — multiple meeting times per section", () => {
211237
// S_MWF_LECTURE_TR_LAB occupies MWF@9 and TR@11. The optional below occupies MWF@9 too.
212238
const conflictingMulti = createMockSection(20, [
213239
{ days: [1, 3, 5], startTime: 900, endTime: 950 }, // conflicts with lecture
214-
{ days: [6], startTime: 800, endTime: 900 }, // Saturday — no conflict
240+
{ days: [6], startTime: 800, endTime: 900 }, // Saturday — no conflict
215241
]);
216242
const optionals = [[conflictingMulti]];
217243
const results = addOptionalCourses(
@@ -243,7 +269,10 @@ describe("addOptionalCourses — numCourses filtering", () => {
243269
1, // numCourses = 1, base already has 1
244270
);
245271
assert.equal(results.length, 1);
246-
assert.deepEqual(results[0].map((s) => s.id), [S_MON_8.id]);
272+
assert.deepEqual(
273+
results[0].map((s) => s.id),
274+
[S_MON_8.id],
275+
);
247276
});
248277

249278
test("numCourses = base + 1 → only schedules with exactly one optional added", () => {
@@ -325,7 +354,14 @@ describe("addOptionalCourses — numCourses filtering", () => {
325354
describe("addOptionalCourses — maxResults cap", () => {
326355
test("respects maxResults = 1", () => {
327356
const optionals = [[S_MON_8], [S_TUE_8], [S_WED_8]];
328-
const results = addOptionalCourses([], BigInt(0), optionals, buildMasks(optionals), undefined, 1);
357+
const results = addOptionalCourses(
358+
[],
359+
BigInt(0),
360+
optionals,
361+
buildMasks(optionals),
362+
undefined,
363+
1,
364+
);
329365
assert.equal(results.length, 1);
330366
});
331367

@@ -337,14 +373,33 @@ describe("addOptionalCourses — maxResults cap", () => {
337373
[S_WED_8],
338374
[createMockSection(50, [{ days: [4], startTime: 800, endTime: 900 }])],
339375
];
340-
const results = addOptionalCourses([], BigInt(0), optionals, buildMasks(optionals), undefined, 5);
376+
const results = addOptionalCourses(
377+
[],
378+
BigInt(0),
379+
optionals,
380+
buildMasks(optionals),
381+
undefined,
382+
5,
383+
);
341384
assert.equal(results.length, 5);
342385
});
343386

344387
test("maxResults larger than total results → returns all", () => {
345388
const optionals = [[S_MON_10], [S_WED_10]];
346-
const uncapped = addOptionalCourses([], BigInt(0), optionals, buildMasks(optionals));
347-
const capped = addOptionalCourses([], BigInt(0), optionals, buildMasks(optionals), undefined, 100);
389+
const uncapped = addOptionalCourses(
390+
[],
391+
BigInt(0),
392+
optionals,
393+
buildMasks(optionals),
394+
);
395+
const capped = addOptionalCourses(
396+
[],
397+
BigInt(0),
398+
optionals,
399+
buildMasks(optionals),
400+
undefined,
401+
100,
402+
);
348403
assert.equal(capped.length, uncapped.length);
349404
});
350405
});
@@ -372,7 +427,12 @@ describe("addOptionalCourses — push/pop isolation", () => {
372427
test("base schedule array is not mutated by the call", () => {
373428
const base = [S_MON_8];
374429
const optionals = [[S_TUE_10]];
375-
addOptionalCourses(base, combinedMask(base), optionals, buildMasks(optionals));
430+
addOptionalCourses(
431+
base,
432+
combinedMask(base),
433+
optionals,
434+
buildMasks(optionals),
435+
);
376436
assert.equal(base.length, 1);
377437
});
378438
});
@@ -385,12 +445,15 @@ describe("addOptionalCourses — push/pop isolation", () => {
385445
describe("locked + optional courses integration", () => {
386446
test("locked courses generate valid base schedules, each extended with optionals", () => {
387447
// Two locked courses, each with 2 sections
388-
const lockedCourseA = [S_MON_8, S_MON_10]; // A sections
389-
const lockedCourseB = [S_TUE_8, S_WED_8]; // B sections (all non-conflicting with A)
390-
const lockedSchedules = generateCombinationsOptimized([lockedCourseA, lockedCourseB]);
448+
const lockedCourseA = [S_MON_8, S_MON_10]; // A sections
449+
const lockedCourseB = [S_TUE_8, S_WED_8]; // B sections (all non-conflicting with A)
450+
const lockedSchedules = generateCombinationsOptimized([
451+
lockedCourseA,
452+
lockedCourseB,
453+
]);
391454

392455
const optionals = [[S_WED_10, S_MON_12]];
393-
const optMasks = buildMasks(optionals);
456+
const optMasks = buildMasks(optionals);
394457

395458
const all: SectionWithCourse[][] = [];
396459
for (const { schedule, mask } of lockedSchedules) {
@@ -402,10 +465,13 @@ describe("locked + optional courses integration", () => {
402465
assert.ok(all.length > 0);
403466
assert.ok(all.every((s) => !hasConflictInSchedule(s)));
404467
// Every result must contain both locked sections
405-
assert.ok(all.every((s) =>
406-
lockedCourseA.some((a) => s.includes(a)) &&
407-
lockedCourseB.some((b) => s.includes(b)),
408-
));
468+
assert.ok(
469+
all.every(
470+
(s) =>
471+
lockedCourseA.some((a) => s.includes(a)) &&
472+
lockedCourseB.some((b) => s.includes(b)),
473+
),
474+
);
409475
});
410476

411477
test("MAX_RESULTS is respected across the combined locked+optional loop", () => {
@@ -415,13 +481,22 @@ describe("locked + optional courses integration", () => {
415481
];
416482
const lockedSchedules = generateCombinationsOptimized(lockedCourses);
417483
const optionals = [[S_WED_8, S_WED_10]];
418-
const optMasks = buildMasks(optionals);
484+
const optMasks = buildMasks(optionals);
419485

420486
const all: SectionWithCourse[][] = [];
421487
for (const { schedule, mask } of lockedSchedules) {
422488
if (all.length >= MAX_RESULTS) break;
423489
const remaining = MAX_RESULTS - all.length;
424-
all.push(...addOptionalCourses(schedule, mask, optionals, optMasks, undefined, remaining));
490+
all.push(
491+
...addOptionalCourses(
492+
schedule,
493+
mask,
494+
optionals,
495+
optMasks,
496+
undefined,
497+
remaining,
498+
),
499+
);
425500
}
426501

427502
assert.ok(all.length <= MAX_RESULTS);
@@ -432,12 +507,14 @@ describe("locked + optional courses integration", () => {
432507
const lockedCourses = [[S_MON_8]];
433508
const lockedSchedules = generateCombinationsOptimized(lockedCourses);
434509
const optionals = [[S_TUE_8], [S_WED_8]];
435-
const optMasks = buildMasks(optionals);
510+
const optMasks = buildMasks(optionals);
436511

437512
// numCourses = 2 → only schedules with exactly 1 optional added
438513
const results: SectionWithCourse[][] = [];
439514
for (const { schedule, mask } of lockedSchedules) {
440-
results.push(...addOptionalCourses(schedule, mask, optionals, optMasks, 2));
515+
results.push(
516+
...addOptionalCourses(schedule, mask, optionals, optMasks, 2),
517+
);
441518
}
442519

443520
assert.ok(results.every((s) => s.length === 2));
@@ -452,7 +529,12 @@ describe("addOptionalCourses — optional course with no sections", () => {
452529
test("empty optional course is silently skipped (no crash)", () => {
453530
const base = [S_MON_8];
454531
const optionals: SectionWithCourse[][] = [[]]; // one optional course, zero sections
455-
const results = addOptionalCourses(base, combinedMask(base), optionals, buildMasks(optionals));
532+
const results = addOptionalCourses(
533+
base,
534+
combinedMask(base),
535+
optionals,
536+
buildMasks(optionals),
537+
);
456538
// The only choice for the empty course is skip → returns [base]
457539
assert.equal(results.length, 1);
458540
assert.deepEqual(results[0], base);
@@ -461,7 +543,12 @@ describe("addOptionalCourses — optional course with no sections", () => {
461543
test("empty optional among non-empty optionals", () => {
462544
const base = [S_MON_8];
463545
const optionals: SectionWithCourse[][] = [[], [S_TUE_10]];
464-
const results = addOptionalCourses(base, combinedMask(base), optionals, buildMasks(optionals));
546+
const results = addOptionalCourses(
547+
base,
548+
combinedMask(base),
549+
optionals,
550+
buildMasks(optionals),
551+
);
465552
// Empty course: skip only. TUE_10 course: skip or include → 2 results
466553
assert.equal(results.length, 2);
467554
assert.ok(results.every((s) => !hasConflictInSchedule(s)));
@@ -476,19 +563,33 @@ describe("addOptionalCourses — exact result IDs", () => {
476563
test("base + one optional: exact section IDs in each result", () => {
477564
const base = [S_MON_8];
478565
const optionals = [[S_TUE_10]];
479-
const results = addOptionalCourses(base, combinedMask(base), optionals, buildMasks(optionals));
566+
const results = addOptionalCourses(
567+
base,
568+
combinedMask(base),
569+
optionals,
570+
buildMasks(optionals),
571+
);
480572
const idSets = toIdSets(results);
481573
// Result 1: just base
482574
assert.ok(idSets.some((s) => s.size === 1 && s.has(S_MON_8.id)));
483575
// Result 2: base + TUE_10
484-
assert.ok(idSets.some((s) => s.size === 2 && s.has(S_MON_8.id) && s.has(S_TUE_10.id)));
576+
assert.ok(
577+
idSets.some(
578+
(s) => s.size === 2 && s.has(S_MON_8.id) && s.has(S_TUE_10.id),
579+
),
580+
);
485581
});
486582

487583
test("one optional with two sections: only the non-conflicting section appears", () => {
488584
const base = [S_MON_8];
489585
// S_MON_8_B conflicts with base; S_TUE_10 does not
490586
const optionals = [[S_MON_8_B, S_TUE_10]];
491-
const results = addOptionalCourses(base, combinedMask(base), optionals, buildMasks(optionals));
587+
const results = addOptionalCourses(
588+
base,
589+
combinedMask(base),
590+
optionals,
591+
buildMasks(optionals),
592+
);
492593
const idSets = toIdSets(results);
493594
// No result should contain S_MON_8_B
494595
assert.ok(idSets.every((s) => !s.has(S_MON_8_B.id)));
@@ -499,11 +600,17 @@ describe("addOptionalCourses — exact result IDs", () => {
499600
test("two optionals: all 4 exact combinations present", () => {
500601
const base = [S_MON_8];
501602
const optionals = [[S_TUE_10], [S_WED_8]];
502-
const results = addOptionalCourses(base, combinedMask(base), optionals, buildMasks(optionals));
603+
const results = addOptionalCourses(
604+
base,
605+
combinedMask(base),
606+
optionals,
607+
buildMasks(optionals),
608+
);
503609
const idSets = toIdSets(results);
504610
assert.equal(idSets.length, 4);
505611
// Exact 4 combinations
506-
const has = (ids: number[]) => idSets.some((s) => s.size === ids.length && ids.every((id) => s.has(id)));
612+
const has = (ids: number[]) =>
613+
idSets.some((s) => s.size === ids.length && ids.every((id) => s.has(id)));
507614
assert.ok(has([S_MON_8.id]));
508615
assert.ok(has([S_MON_8.id, S_TUE_10.id]));
509616
assert.ok(has([S_MON_8.id, S_WED_8.id]));
@@ -519,13 +626,13 @@ describe("generateCombinationsOptimized — capped results are a valid subset",
519626
test("every schedule in the capped result also appears in the uncapped result", () => {
520627
// Small enough that uncapped is tractable
521628
const sectionsByCourse = [
522-
[S_MON_8, S_MON_10, S_MON_12],
523-
[S_TUE_8, S_TUE_10],
524-
[S_WED_8, S_WED_10],
629+
[S_MON_8, S_MON_10, S_MON_12],
630+
[S_TUE_8, S_TUE_10],
631+
[S_WED_8, S_WED_10],
525632
];
526633

527634
const uncapped = generateCombinationsOptimized(sectionsByCourse);
528-
const capped = generateCombinationsOptimized(sectionsByCourse, 3);
635+
const capped = generateCombinationsOptimized(sectionsByCourse, 3);
529636

530637
assert.ok(capped.length <= 3);
531638
assert.ok(capped.length <= uncapped.length);
@@ -539,7 +646,10 @@ describe("generateCombinationsOptimized — capped results are a valid subset",
539646
for (const id of ids) if (!u.has(id)) return false;
540647
return true;
541648
});
542-
assert.ok(found, `capped schedule [${[...ids]}] not found in uncapped results`);
649+
assert.ok(
650+
found,
651+
`capped schedule [${[...ids]}] not found in uncapped results`,
652+
);
543653
}
544654
});
545655

@@ -549,6 +659,8 @@ describe("generateCombinationsOptimized — capped results are a valid subset",
549659
[S_WED_8, S_WED_10],
550660
];
551661
const results = generateCombinationsOptimized(sectionsByCourse, 5);
552-
assert.ok(results.every(({ schedule }) => !hasConflictInSchedule(schedule)));
662+
assert.ok(
663+
results.every(({ schedule }) => !hasConflictInSchedule(schedule)),
664+
);
553665
});
554666
});

0 commit comments

Comments
 (0)