punctilio 3.2.3 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Shared test utilities for punctilio test suites.
3
+ */
4
+
5
+ import seedrandom from "seedrandom"
6
+
7
+ /**
8
+ * Fragments covering every transform type: quotes, dashes, apostrophes,
9
+ * ellipses, multiplication, math symbols, legal symbols, arrows, primes,
10
+ * fractions, degrees, ordinals, nbsp-triggering short words, and plain text.
11
+ */
12
+ const FRAGMENTS = [
13
+ `"Hello," she said. `,
14
+ `It's a beautiful day -- isn't it? `,
15
+ `Wait... `,
16
+ `The temperature was 20 C. `,
17
+ `5x5 != 25. `,
18
+ `Add 1/2 cup. `,
19
+ `He's 5'10" tall. `,
20
+ `(c) 2024 Acme Corp. `,
21
+ `See p. 42 for details. `,
22
+ `Dr. Smith arrived. `,
23
+ `The 1st place winner. `,
24
+ `Go -> click -> done. `,
25
+ `Really?! No way!! `,
26
+ `A B C D E F G H. `,
27
+ `Pages 10-20 contain important info. `,
28
+ `January-March was cold. `,
29
+ `The value is -5. `,
30
+ `"Don't stop," he said, "keep going." `,
31
+ `J. K. Rowling wrote it. `,
32
+ `100 km in 2 hrs. `,
33
+ ]
34
+
35
+ /**
36
+ * Builds a deterministic pseudo-random string of approximately `charCount`
37
+ * characters by randomly sampling from FRAGMENTS using a seeded PRNG.
38
+ *
39
+ * @param charCount - Approximate desired character count
40
+ * @param seed - PRNG seed (default: "42")
41
+ */
42
+ export function buildMixedContent(charCount: number, seed: string = "42"): string {
43
+ const random = seedrandom(seed)
44
+ let result = ""
45
+ while (result.length < charCount) {
46
+ const index = Math.floor(random() * FRAGMENTS.length)
47
+ result += FRAGMENTS[index]
48
+ }
49
+ return result
50
+ }
51
+
52
+ /**
53
+ * Asserts that a function scales linearly (not quadratically) with input size.
54
+ *
55
+ * Measures ms/char at 4 input sizes (1x, 2x, 4x, 8x) after a JIT warmup pass.
56
+ * Compares the two largest sizes (4x vs 8x) which have the best signal-to-noise
57
+ * ratio. For a linear algorithm, doubling input keeps ms/char constant (ratio ~1).
58
+ * For a quadratic one, doubling input doubles ms/char (ratio ~2).
59
+ *
60
+ * Default startingN of 5000 produces inputs from ~5K to ~40K chars, which is
61
+ * past V8's string allocation overhead inflection point where ms/char stabilizes.
62
+ *
63
+ * Uses min 3 iterations and min 20ms per size to get stable averages.
64
+ *
65
+ * @param fn - The function to benchmark
66
+ * @param buildInput - Builds an input string of approximately `n` units
67
+ * @param startingN - Size parameter for the smallest input (default: 5000)
68
+ */
69
+ export function assertLinearScaling(
70
+ fn: (input: string) => unknown,
71
+ buildInput: (n: number) => string,
72
+ startingN: number = 5000
73
+ ): void {
74
+ const multipliers = [1, 2, 4, 8]
75
+
76
+ // Warmup with the largest input so JIT compiles all code paths before timing.
77
+ fn(buildInput(startingN * multipliers[multipliers.length - 1]))
78
+
79
+ function measureMsPerChar(mult: number): number {
80
+ const input = buildInput(startingN * mult)
81
+ const minIterations = 3
82
+ const minElapsedMs = 20
83
+ let totalElapsed = 0
84
+ let iterations = 0
85
+ while (iterations < minIterations || totalElapsed < minElapsedMs) {
86
+ const start = performance.now()
87
+ fn(input)
88
+ totalElapsed += performance.now() - start
89
+ iterations++
90
+ }
91
+ return totalElapsed / iterations / input.length
92
+ }
93
+
94
+ // Measure all sizes (exercises the function at various scales)
95
+ const msPerChar = multipliers.map(measureMsPerChar)
96
+
97
+ // Compare the two largest inputs (best SNR, per-call overhead is negligible).
98
+ // For linear: rate_8x / rate_4x ≈ 1. For quadratic: ≈ 2.
99
+ const rate4x = msPerChar[2]
100
+ const rate8x = msPerChar[3]
101
+ expect(rate8x / rate4x).toBeLessThan(1.5)
102
+ }
@@ -1,5 +1,6 @@
1
+ import { jest } from "@jest/globals"
1
2
  import { assertSeparatorAbsent, countSeparators, assertSeparatorCountPreserved, formatErrorString } from "../utils.js"
2
- import { DEFAULT_SEPARATOR, cachedRegExp } from "../constants.js"
3
+ import { DEFAULT_SEPARATOR, cachedRegExp, clearRegexCache } from "../constants.js"
3
4
 
4
5
  describe("assertSeparatorAbsent", () => {
5
6
  it.each([
@@ -76,6 +77,10 @@ describe("assertSeparatorCountPreserved", () => {
76
77
  })
77
78
 
78
79
  describe("cachedRegExp", () => {
80
+ afterEach(() => {
81
+ clearRegexCache()
82
+ })
83
+
79
84
  it("evicts oldest entry when cache exceeds max size", () => {
80
85
  // Fill the cache well beyond 1000 entries with unique patterns
81
86
  for (let i = 0; i < 1010; i++) {
@@ -89,20 +94,19 @@ describe("cachedRegExp", () => {
89
94
  })
90
95
 
91
96
  describe("formatErrorString", () => {
92
- let originalWrite: typeof process.stderr.write
93
97
  let stderrOutput: string[]
98
+ let stderrSpy: jest.SpiedFunction<typeof process.stderr.write>
94
99
 
95
100
  beforeEach(() => {
96
- originalWrite = process.stderr.write
97
101
  stderrOutput = []
98
- process.stderr.write = ((chunk: string) => {
99
- stderrOutput.push(chunk)
102
+ stderrSpy = jest.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => {
103
+ stderrOutput.push(String(chunk))
100
104
  return true
101
- }) as typeof process.stderr.write
105
+ })
102
106
  })
103
107
 
104
108
  afterEach(() => {
105
- process.stderr.write = originalWrite
109
+ stderrSpy.mockRestore()
106
110
  })
107
111
 
108
112
  it("returns JSON-stringified content for short strings", () => {