@hegeldev/hegel 0.1.0

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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/binary.d.ts +27 -0
  4. package/dist/binary.d.ts.map +1 -0
  5. package/dist/binary.js +50 -0
  6. package/dist/binary.js.map +1 -0
  7. package/dist/collections.d.ts +114 -0
  8. package/dist/collections.d.ts.map +1 -0
  9. package/dist/collections.js +313 -0
  10. package/dist/collections.js.map +1 -0
  11. package/dist/combinators.d.ts +34 -0
  12. package/dist/combinators.d.ts.map +1 -0
  13. package/dist/combinators.js +152 -0
  14. package/dist/combinators.js.map +1 -0
  15. package/dist/conformance.d.ts +31 -0
  16. package/dist/conformance.d.ts.map +1 -0
  17. package/dist/conformance.js +60 -0
  18. package/dist/conformance.js.map +1 -0
  19. package/dist/connection.d.ts +82 -0
  20. package/dist/connection.d.ts.map +1 -0
  21. package/dist/connection.js +231 -0
  22. package/dist/connection.js.map +1 -0
  23. package/dist/crc32.d.ts +13 -0
  24. package/dist/crc32.d.ts.map +1 -0
  25. package/dist/crc32.js +30 -0
  26. package/dist/crc32.js.map +1 -0
  27. package/dist/derive.d.ts +225 -0
  28. package/dist/derive.d.ts.map +1 -0
  29. package/dist/derive.js +296 -0
  30. package/dist/derive.js.map +1 -0
  31. package/dist/embedded.d.ts +38 -0
  32. package/dist/embedded.d.ts.map +1 -0
  33. package/dist/embedded.js +237 -0
  34. package/dist/embedded.js.map +1 -0
  35. package/dist/floats.d.ts +57 -0
  36. package/dist/floats.d.ts.map +1 -0
  37. package/dist/floats.js +100 -0
  38. package/dist/floats.js.map +1 -0
  39. package/dist/formats.d.ts +62 -0
  40. package/dist/formats.d.ts.map +1 -0
  41. package/dist/formats.js +164 -0
  42. package/dist/formats.js.map +1 -0
  43. package/dist/generator.d.ts +80 -0
  44. package/dist/generator.d.ts.map +1 -0
  45. package/dist/generator.js +128 -0
  46. package/dist/generator.js.map +1 -0
  47. package/dist/generators/collections.d.ts +20 -0
  48. package/dist/generators/collections.d.ts.map +1 -0
  49. package/dist/generators/collections.js +209 -0
  50. package/dist/generators/collections.js.map +1 -0
  51. package/dist/generators/combinators.d.ts +15 -0
  52. package/dist/generators/combinators.d.ts.map +1 -0
  53. package/dist/generators/combinators.js +143 -0
  54. package/dist/generators/combinators.js.map +1 -0
  55. package/dist/generators/compose.d.ts +27 -0
  56. package/dist/generators/compose.d.ts.map +1 -0
  57. package/dist/generators/compose.js +82 -0
  58. package/dist/generators/compose.js.map +1 -0
  59. package/dist/generators/core.d.ts +53 -0
  60. package/dist/generators/core.d.ts.map +1 -0
  61. package/dist/generators/core.js +134 -0
  62. package/dist/generators/core.js.map +1 -0
  63. package/dist/generators/index.d.ts +18 -0
  64. package/dist/generators/index.d.ts.map +1 -0
  65. package/dist/generators/index.js +15 -0
  66. package/dist/generators/index.js.map +1 -0
  67. package/dist/generators/numeric.d.ts +40 -0
  68. package/dist/generators/numeric.d.ts.map +1 -0
  69. package/dist/generators/numeric.js +128 -0
  70. package/dist/generators/numeric.js.map +1 -0
  71. package/dist/generators/primitives.d.ts +138 -0
  72. package/dist/generators/primitives.d.ts.map +1 -0
  73. package/dist/generators/primitives.js +240 -0
  74. package/dist/generators/primitives.js.map +1 -0
  75. package/dist/generators/strings.d.ts +73 -0
  76. package/dist/generators/strings.d.ts.map +1 -0
  77. package/dist/generators/strings.js +215 -0
  78. package/dist/generators/strings.js.map +1 -0
  79. package/dist/generators/tuples.d.ts +11 -0
  80. package/dist/generators/tuples.d.ts.map +1 -0
  81. package/dist/generators/tuples.js +43 -0
  82. package/dist/generators/tuples.js.map +1 -0
  83. package/dist/generators.d.ts +408 -0
  84. package/dist/generators.d.ts.map +1 -0
  85. package/dist/generators.js +898 -0
  86. package/dist/generators.js.map +1 -0
  87. package/dist/index.d.ts +16 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +13 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/install.d.ts +6 -0
  92. package/dist/install.d.ts.map +1 -0
  93. package/dist/install.js +91 -0
  94. package/dist/install.js.map +1 -0
  95. package/dist/integers.d.ts +37 -0
  96. package/dist/integers.d.ts.map +1 -0
  97. package/dist/integers.js +63 -0
  98. package/dist/integers.js.map +1 -0
  99. package/dist/labels.d.ts +21 -0
  100. package/dist/labels.d.ts.map +1 -0
  101. package/dist/labels.js +20 -0
  102. package/dist/labels.js.map +1 -0
  103. package/dist/objects.d.ts +39 -0
  104. package/dist/objects.d.ts.map +1 -0
  105. package/dist/objects.js +98 -0
  106. package/dist/objects.js.map +1 -0
  107. package/dist/primitives.d.ts +14 -0
  108. package/dist/primitives.d.ts.map +1 -0
  109. package/dist/primitives.js +51 -0
  110. package/dist/primitives.js.map +1 -0
  111. package/dist/protocol.d.ts +25 -0
  112. package/dist/protocol.d.ts.map +1 -0
  113. package/dist/protocol.js +77 -0
  114. package/dist/protocol.js.map +1 -0
  115. package/dist/runner.d.ts +115 -0
  116. package/dist/runner.d.ts.map +1 -0
  117. package/dist/runner.js +455 -0
  118. package/dist/runner.js.map +1 -0
  119. package/dist/session.d.ts +19 -0
  120. package/dist/session.d.ts.map +1 -0
  121. package/dist/session.js +157 -0
  122. package/dist/session.js.map +1 -0
  123. package/dist/spans.d.ts +23 -0
  124. package/dist/spans.d.ts.map +1 -0
  125. package/dist/spans.js +51 -0
  126. package/dist/spans.js.map +1 -0
  127. package/dist/strings.d.ts +67 -0
  128. package/dist/strings.d.ts.map +1 -0
  129. package/dist/strings.js +107 -0
  130. package/dist/strings.js.map +1 -0
  131. package/dist/testCase.d.ts +118 -0
  132. package/dist/testCase.d.ts.map +1 -0
  133. package/dist/testCase.js +186 -0
  134. package/dist/testCase.js.map +1 -0
  135. package/dist/uv-install.sh +2226 -0
  136. package/dist/uv.d.ts +20 -0
  137. package/dist/uv.d.ts.map +1 -0
  138. package/dist/uv.js +103 -0
  139. package/dist/uv.js.map +1 -0
  140. package/dist/wtf8.d.ts +16 -0
  141. package/dist/wtf8.d.ts.map +1 -0
  142. package/dist/wtf8.js +52 -0
  143. package/dist/wtf8.js.map +1 -0
  144. package/package.json +60 -0
@@ -0,0 +1,77 @@
1
+ import { addExtension } from "cbor-x";
2
+ import { crc32 } from "./crc32.js";
3
+ import { wtf8ToString } from "./wtf8.js";
4
+ export const MAGIC = 0x4845474c;
5
+ export const HEADER_SIZE = 20;
6
+ export const TERMINATOR = 0x0a;
7
+ export const REPLY_BIT = 0x80000000;
8
+ export const CLOSE_STREAM_MESSAGE_ID = 0x7fffffff;
9
+ export const CLOSE_STREAM_PAYLOAD = Buffer.from([0xfe]);
10
+ export const HANDSHAKE_STRING = "hegel_handshake_start";
11
+ // cbor-x requires a Class for addExtension, but we only use the decode
12
+ // path (tag 91 is sent by the server, never by the client).
13
+ addExtension({
14
+ /* v8 ignore start */
15
+ Class: class HegelString {
16
+ },
17
+ tag: 91,
18
+ encode: () => Buffer.alloc(0),
19
+ /* v8 ignore stop */
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ decode(data) {
22
+ if (Buffer.isBuffer(data))
23
+ return wtf8ToString(data);
24
+ if (data instanceof Uint8Array)
25
+ return wtf8ToString(Buffer.from(data));
26
+ return String(data);
27
+ },
28
+ });
29
+ /**
30
+ * Encode a packet into a single Buffer ready for writing.
31
+ */
32
+ export function encodePacket(packet) {
33
+ const messageIdRaw = packet.isReply ? (packet.messageId | REPLY_BIT) >>> 0 : packet.messageId;
34
+ const header = Buffer.alloc(HEADER_SIZE);
35
+ header.writeUInt32BE(MAGIC, 0);
36
+ // checksum placeholder at offset 4 (already 0)
37
+ header.writeUInt32BE(packet.streamId, 8);
38
+ header.writeUInt32BE(messageIdRaw, 12);
39
+ header.writeUInt32BE(packet.payload.length, 16);
40
+ // CRC32 over header (checksum zeroed) + payload
41
+ const checksum = crc32(Buffer.concat([header, packet.payload]));
42
+ header.writeUInt32BE(checksum, 4);
43
+ return Buffer.concat([header, packet.payload, Buffer.from([TERMINATOR])]);
44
+ }
45
+ /**
46
+ * Decode a packet from raw bytes.
47
+ *
48
+ * @param readExact - Function that synchronously reads exactly `n` bytes.
49
+ * @returns The decoded packet.
50
+ */
51
+ export function readPacketFrom(readExact) {
52
+ const header = readExact(HEADER_SIZE);
53
+ const magic = header.readUInt32BE(0);
54
+ if (magic !== MAGIC) {
55
+ throw new Error(`Invalid magic: expected 0x${MAGIC.toString(16)}, got 0x${magic.toString(16)}`);
56
+ }
57
+ const checksum = header.readUInt32BE(4);
58
+ const streamId = header.readUInt32BE(8);
59
+ const messageIdRaw = header.readUInt32BE(12);
60
+ const payloadLength = header.readUInt32BE(16);
61
+ const isReply = (messageIdRaw & REPLY_BIT) !== 0;
62
+ const messageId = messageIdRaw & ~REPLY_BIT;
63
+ const payload = readExact(payloadLength);
64
+ const terminator = readExact(1);
65
+ if (terminator[0] !== TERMINATOR) {
66
+ throw new Error(`Invalid terminator: expected 0x${TERMINATOR.toString(16)}, got 0x${terminator[0].toString(16)}`);
67
+ }
68
+ // Verify CRC32
69
+ const headerForCheck = Buffer.from(header);
70
+ headerForCheck.writeUInt32BE(0, 4); // zero out checksum field
71
+ const computed = crc32(Buffer.concat([headerForCheck, payload]));
72
+ if (computed !== checksum) {
73
+ throw new Error(`CRC32 mismatch: expected 0x${checksum.toString(16)}, got 0x${computed.toString(16)}`);
74
+ }
75
+ return { streamId, messageId, isReply, payload };
76
+ }
77
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Test runner: Hegel builder, Settings, and test lifecycle.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ import { TestCase, type DataSource } from "./testCase.js";
7
+ import type { Connection, Stream } from "./connection.js";
8
+ export declare enum Verbosity {
9
+ Quiet = "quiet",
10
+ Normal = "normal",
11
+ Verbose = "verbose",
12
+ Debug = "debug"
13
+ }
14
+ export declare enum HealthCheck {
15
+ FilterTooMuch = "filter_too_much",
16
+ TooSlow = "too_slow",
17
+ TestCasesTooLarge = "test_cases_too_large",
18
+ LargeInitialTestCase = "large_initial_test_case"
19
+ }
20
+ export type Database = {
21
+ kind: "unset";
22
+ } | {
23
+ kind: "disabled";
24
+ } | {
25
+ kind: "path";
26
+ path: string;
27
+ };
28
+ export declare const Database: {
29
+ unset: Database;
30
+ disabled: Database;
31
+ fromPath: (path: string) => Database;
32
+ };
33
+ export interface Settings {
34
+ testCases: number;
35
+ seed: number | null;
36
+ verbosity: Verbosity;
37
+ derandomize: boolean;
38
+ database: Database;
39
+ suppressHealthCheck: HealthCheck[];
40
+ }
41
+ export declare function defaultSettings(): Settings;
42
+ /**
43
+ * DataSource implementation that communicates with the hegel server
44
+ * over a multiplexed stream connection.
45
+ */
46
+ export declare class ServerDataSource implements DataSource {
47
+ private stream;
48
+ private connection;
49
+ private _aborted;
50
+ constructor(connection: Connection, stream: Stream);
51
+ private sendRequest;
52
+ generate(schema: Record<string, unknown>): unknown;
53
+ startSpan(label: number): void;
54
+ stopSpan(discard: boolean): void;
55
+ newCollection(minSize: number, maxSize?: number): number;
56
+ collectionMore(collectionId: number): boolean;
57
+ collectionReject(collectionId: number, why?: string): void;
58
+ markComplete(status: string, origin: string | null): void;
59
+ testAborted(): boolean;
60
+ }
61
+ export type TestCaseResult = {
62
+ status: "valid";
63
+ } | {
64
+ status: "invalid";
65
+ } | {
66
+ status: "interesting";
67
+ error: unknown;
68
+ };
69
+ export declare function runTestCase(dataSource: DataSource, testFn: (tc: TestCase) => void, isFinal: boolean): TestCaseResult;
70
+ export interface TestLocation {
71
+ function: string;
72
+ file: string;
73
+ class: string;
74
+ beginLine: number;
75
+ }
76
+ export declare class Hegel {
77
+ private testFn;
78
+ private _settings;
79
+ private _databaseKey;
80
+ private _testLocation;
81
+ constructor(testFn: (tc: TestCase) => void);
82
+ /** Override default settings. Returns this for chaining. */
83
+ settings(s: Partial<Settings>): this;
84
+ /** Set the database key for persisting failing examples across runs. */
85
+ databaseKey(key: string): this;
86
+ /** Set the test location for Antithesis integration. */
87
+ testLocation(location: TestLocation): this;
88
+ /**
89
+ * Execute the property-based test.
90
+ *
91
+ * Connects to the hegel server (spawning it on first use), runs the
92
+ * configured number of test cases, and throws if any test case fails.
93
+ * On failure, the failing input is shrunk to a minimal example and
94
+ * replayed with draw output printed to stderr.
95
+ */
96
+ run(): void;
97
+ }
98
+ /**
99
+ * Wrap a property-based test body into a function suitable for a test runner.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * import { test } from 'vitest';
104
+ * import * as hegel from 'hegel';
105
+ * import * as gs from 'hegel/generators';
106
+ *
107
+ * test('addition is commutative', hegel.test((tc) => {
108
+ * const x = tc.draw(gs.integers());
109
+ * const y = tc.draw(gs.integers());
110
+ * expect(x + y).toBe(y + x);
111
+ * }));
112
+ * ```
113
+ */
114
+ export declare function test(testFn: (tc: TestCase) => void, settings?: Partial<Settings>): () => void;
115
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
package/dist/runner.js ADDED
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Test runner: Hegel builder, Settings, and test lifecycle.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { HegelSession } from "./session.js";
9
+ import { TestCase, StopTestError, AssumeError } from "./testCase.js";
10
+ import { encode, decode } from "cbor-x";
11
+ export var Verbosity;
12
+ (function (Verbosity) {
13
+ Verbosity["Quiet"] = "quiet";
14
+ Verbosity["Normal"] = "normal";
15
+ Verbosity["Verbose"] = "verbose";
16
+ Verbosity["Debug"] = "debug";
17
+ })(Verbosity || (Verbosity = {}));
18
+ export var HealthCheck;
19
+ (function (HealthCheck) {
20
+ HealthCheck["FilterTooMuch"] = "filter_too_much";
21
+ HealthCheck["TooSlow"] = "too_slow";
22
+ HealthCheck["TestCasesTooLarge"] = "test_cases_too_large";
23
+ HealthCheck["LargeInitialTestCase"] = "large_initial_test_case";
24
+ })(HealthCheck || (HealthCheck = {}));
25
+ export const Database = {
26
+ unset: { kind: "unset" },
27
+ disabled: { kind: "disabled" },
28
+ fromPath: (path) => ({ kind: "path", path }),
29
+ };
30
+ function isInCI() {
31
+ const ciVars = [
32
+ ["CI", null],
33
+ ["BITBUCKET_COMMIT", null],
34
+ ["BUILDKITE", "true"],
35
+ ["CIRCLECI", "true"],
36
+ ["CIRRUS_CI", "true"],
37
+ ["CODEBUILD_BUILD_ID", null],
38
+ ["GITHUB_ACTIONS", "true"],
39
+ ["GITLAB_CI", null],
40
+ ["HEROKU_TEST_RUN_ID", null],
41
+ ["TEAMCITY_VERSION", null],
42
+ ["TF_BUILD", "true"],
43
+ ["bamboo.buildKey", null],
44
+ ];
45
+ return ciVars.some(([key, value]) => {
46
+ if (value === null) {
47
+ return process.env[key] !== undefined;
48
+ }
49
+ return process.env[key] === value;
50
+ });
51
+ }
52
+ export function defaultSettings() {
53
+ const inCI = isInCI();
54
+ return {
55
+ testCases: 100,
56
+ seed: null,
57
+ verbosity: Verbosity.Normal,
58
+ derandomize: inCI,
59
+ database: inCI ? Database.disabled : Database.unset,
60
+ suppressHealthCheck: [],
61
+ };
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // ServerDataSource
65
+ // ---------------------------------------------------------------------------
66
+ function isRecord(value) {
67
+ return value !== null && typeof value === "object" && !Array.isArray(value);
68
+ }
69
+ /**
70
+ * DataSource implementation that communicates with the hegel server
71
+ * over a multiplexed stream connection.
72
+ */
73
+ export class ServerDataSource {
74
+ stream;
75
+ connection;
76
+ _aborted = false;
77
+ constructor(connection, stream) {
78
+ this.connection = connection;
79
+ this.stream = stream;
80
+ }
81
+ sendRequest(command, payload = {}) {
82
+ /* v8 ignore start: reachable via stopSpan after abort, but swallowed by catch */
83
+ if (this._aborted) {
84
+ throw new StopTestError();
85
+ }
86
+ /* v8 ignore stop */
87
+ const message = { command, ...payload };
88
+ const encoded = encode(message);
89
+ const id = this.stream.sendRequest(encoded);
90
+ const responseBytes = this.stream.receiveReply(id);
91
+ const response = decode(responseBytes);
92
+ /* v8 ignore start: server always returns CBOR maps */
93
+ if (!isRecord(response))
94
+ return response;
95
+ /* v8 ignore stop */
96
+ if ("error" in response) {
97
+ /* v8 ignore start: server always sends type field */
98
+ const errorType = String(response["type"] ?? "");
99
+ /* v8 ignore stop */
100
+ const errorMsg = JSON.stringify(response["error"]);
101
+ if (errorMsg.includes("overflow") ||
102
+ errorMsg.includes("StopTest") ||
103
+ errorType.includes("overflow") ||
104
+ errorType.includes("StopTest")) {
105
+ this.stream.markClosed();
106
+ this._aborted = true;
107
+ throw new StopTestError();
108
+ }
109
+ /* v8 ignore start: FlakyStrategyDefinition is detected in test_done results, not here */
110
+ if (errorMsg.includes("FlakyStrategyDefinition") || errorMsg.includes("FlakyReplay")) {
111
+ this.stream.markClosed();
112
+ this._aborted = true;
113
+ throw new StopTestError();
114
+ }
115
+ /* v8 ignore stop */
116
+ /* v8 ignore start: requires server to crash mid-request */
117
+ if (this.connection.hasServerExited()) {
118
+ throw new Error(`Server process crashed`);
119
+ }
120
+ /* v8 ignore stop */
121
+ throw new Error(`Server error (${errorType}): ${errorMsg}`);
122
+ }
123
+ /* v8 ignore start: server always wraps responses in {result: ...} */
124
+ if ("result" in response) {
125
+ return response["result"];
126
+ }
127
+ return response;
128
+ /* v8 ignore stop */
129
+ }
130
+ generate(schema) {
131
+ return this.sendRequest("generate", { schema });
132
+ }
133
+ startSpan(label) {
134
+ this.sendRequest("start_span", { label });
135
+ }
136
+ stopSpan(discard) {
137
+ this.sendRequest("stop_span", { discard });
138
+ }
139
+ newCollection(minSize, maxSize) {
140
+ const payload = { min_size: minSize };
141
+ if (maxSize !== undefined) {
142
+ payload["max_size"] = maxSize;
143
+ }
144
+ const result = this.sendRequest("new_collection", payload);
145
+ /* v8 ignore start: server always returns integer for new_collection */
146
+ if (typeof result !== "number")
147
+ throw new Error(`Expected integer from new_collection, got ${typeof result}`);
148
+ /* v8 ignore stop */
149
+ return result;
150
+ }
151
+ collectionMore(collectionId) {
152
+ const result = this.sendRequest("collection_more", {
153
+ collection_id: collectionId,
154
+ });
155
+ /* v8 ignore start: server always returns boolean for collection_more */
156
+ if (typeof result !== "boolean")
157
+ throw new Error(`Expected boolean from collection_more, got ${typeof result}`);
158
+ /* v8 ignore stop */
159
+ return result;
160
+ }
161
+ collectionReject(collectionId, why) {
162
+ const payload = {
163
+ collection_id: collectionId,
164
+ };
165
+ /* v8 ignore start: callers always provide why */
166
+ if (why !== undefined) {
167
+ payload["why"] = why;
168
+ }
169
+ /* v8 ignore stop */
170
+ this.sendRequest("collection_reject", payload);
171
+ }
172
+ markComplete(status, origin) {
173
+ try {
174
+ const message = {
175
+ command: "mark_complete",
176
+ status,
177
+ origin: origin ?? null,
178
+ };
179
+ const encoded = encode(message);
180
+ const id = this.stream.sendRequest(encoded);
181
+ this.stream.receiveReply(id);
182
+ }
183
+ catch {
184
+ // ignore errors during mark_complete
185
+ }
186
+ this.stream.close();
187
+ }
188
+ testAborted() {
189
+ return this._aborted;
190
+ }
191
+ }
192
+ function extractOrigin(error) {
193
+ if (!(error instanceof Error) || !error.stack)
194
+ return null;
195
+ const lines = error.stack.split("\n");
196
+ for (const line of lines) {
197
+ const trimmed = line.trim();
198
+ if (trimmed.startsWith("at ") && !trimmed.includes("node_modules")) {
199
+ return trimmed;
200
+ }
201
+ }
202
+ /* v8 ignore start: all stack traces in practice have at least one non-node_modules frame */
203
+ return null;
204
+ /* v8 ignore stop */
205
+ }
206
+ export function runTestCase(dataSource, testFn, isFinal) {
207
+ const tc = new TestCase(dataSource, isFinal);
208
+ let result;
209
+ let origin = null;
210
+ try {
211
+ testFn(tc);
212
+ result = { status: "valid" };
213
+ }
214
+ catch (e) {
215
+ if (e instanceof AssumeError) {
216
+ result = { status: "invalid" };
217
+ }
218
+ else if (e instanceof StopTestError) {
219
+ result = { status: "invalid" };
220
+ }
221
+ else {
222
+ result = { status: "interesting", error: e };
223
+ origin = extractOrigin(e);
224
+ if (isFinal) {
225
+ const msg = e instanceof Error ? e.message : String(e);
226
+ console.error(`\n${msg}`);
227
+ if (e instanceof Error && e.stack) {
228
+ console.error(e.stack);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ // Send mark_complete unless test was aborted (server already closed stream)
234
+ if (!tc.testAborted) {
235
+ const status = result.status === "valid" ? "VALID" : result.status === "invalid" ? "INVALID" : "INTERESTING";
236
+ dataSource.markComplete(status, origin);
237
+ }
238
+ return result;
239
+ }
240
+ /* v8 ignore start: only runs inside Antithesis */
241
+ function isRunningInAntithesis() {
242
+ const dir = process.env["ANTITHESIS_OUTPUT_DIR"];
243
+ return dir !== undefined && dir !== "";
244
+ }
245
+ function emitAntithesisAssertion(location, passed) {
246
+ const dir = process.env["ANTITHESIS_OUTPUT_DIR"];
247
+ if (!dir)
248
+ return;
249
+ const filePath = path.join(dir, "sdk.jsonl");
250
+ const id = `${location.class}::${location.function} passes properties`;
251
+ const locationObj = {
252
+ class: location.class,
253
+ function: location.function,
254
+ file: location.file,
255
+ begin_line: location.beginLine,
256
+ begin_column: 0,
257
+ };
258
+ const declaration = {
259
+ antithesis_assert: {
260
+ hit: false,
261
+ must_hit: true,
262
+ assert_type: "always",
263
+ display_type: "Always",
264
+ condition: false,
265
+ id,
266
+ message: id,
267
+ location: locationObj,
268
+ },
269
+ };
270
+ const evaluation = {
271
+ antithesis_assert: {
272
+ hit: true,
273
+ must_hit: true,
274
+ assert_type: "always",
275
+ display_type: "Always",
276
+ condition: passed,
277
+ id,
278
+ message: id,
279
+ location: locationObj,
280
+ },
281
+ };
282
+ fs.appendFileSync(filePath, JSON.stringify(declaration) + "\n" + JSON.stringify(evaluation) + "\n");
283
+ /* v8 ignore stop */
284
+ }
285
+ export class Hegel {
286
+ testFn;
287
+ _settings;
288
+ _databaseKey = null;
289
+ _testLocation = null;
290
+ constructor(testFn) {
291
+ this.testFn = testFn;
292
+ this._settings = defaultSettings();
293
+ }
294
+ /** Override default settings. Returns this for chaining. */
295
+ settings(s) {
296
+ Object.assign(this._settings, s);
297
+ return this;
298
+ }
299
+ /** Set the database key for persisting failing examples across runs. */
300
+ databaseKey(key) {
301
+ this._databaseKey = key;
302
+ return this;
303
+ }
304
+ /** Set the test location for Antithesis integration. */
305
+ /* v8 ignore start: only used inside Antithesis */
306
+ testLocation(location) {
307
+ this._testLocation = location;
308
+ return this;
309
+ }
310
+ /* v8 ignore stop */
311
+ /**
312
+ * Execute the property-based test.
313
+ *
314
+ * Connects to the hegel server (spawning it on first use), runs the
315
+ * configured number of test cases, and throws if any test case fails.
316
+ * On failure, the failing input is shrunk to a minimal example and
317
+ * replayed with draw output printed to stderr.
318
+ */
319
+ run() {
320
+ const session = HegelSession.get();
321
+ const connection = session.connection;
322
+ const testStream = connection.newStream();
323
+ // Build run_test message
324
+ const suppressNames = this._settings.suppressHealthCheck.map((c) => c);
325
+ const databaseKeyValue = this._databaseKey !== null ? Buffer.from(this._databaseKey, "utf-8") : null;
326
+ const runTestMsg = {
327
+ command: "run_test",
328
+ test_cases: this._settings.testCases,
329
+ seed: this._settings.seed,
330
+ stream_id: testStream.streamId,
331
+ database_key: databaseKeyValue,
332
+ derandomize: this._settings.derandomize,
333
+ };
334
+ // Database field
335
+ if (this._settings.database.kind === "disabled") {
336
+ runTestMsg["database"] = null;
337
+ }
338
+ else if (this._settings.database.kind === "path") {
339
+ runTestMsg["database"] = this._settings.database.path;
340
+ }
341
+ if (suppressNames.length > 0) {
342
+ runTestMsg["suppress_health_check"] = suppressNames;
343
+ }
344
+ // Send run_test on control stream
345
+ const controlPayload = encode(runTestMsg);
346
+ const reqId = session.controlStream.sendRequest(controlPayload);
347
+ session.controlStream.receiveReply(reqId);
348
+ // Event loop
349
+ let resultData;
350
+ const ackNull = encode({ result: null });
351
+ while (true) {
352
+ const [eventId, eventPayload] = testStream.receiveRequest();
353
+ const event = decode(eventPayload);
354
+ const eventType = event["event"];
355
+ if (eventType === "test_case") {
356
+ const streamId = event["stream_id"];
357
+ const testCaseStream = connection.connectStream(streamId);
358
+ // Ack BEFORE running the test
359
+ testStream.writeReply(eventId, ackNull);
360
+ const ds = new ServerDataSource(connection, testCaseStream);
361
+ runTestCase(ds, this.testFn, false);
362
+ // Track interesting cases (server uses this for final replay decisions)
363
+ }
364
+ else {
365
+ /* v8 ignore start: server only sends test_case and test_done events */
366
+ if (eventType !== "test_done") {
367
+ throw new Error(`Unknown event: ${eventType}`);
368
+ }
369
+ /* v8 ignore stop */
370
+ const ackTrue = encode({ result: true });
371
+ testStream.writeReply(eventId, ackTrue);
372
+ /* v8 ignore start: server always sends results object */
373
+ resultData = event["results"] ?? {};
374
+ /* v8 ignore stop */
375
+ break;
376
+ }
377
+ }
378
+ // Check for server-side errors
379
+ /* v8 ignore start: requires server to report error in test_done results */
380
+ if (resultData["error"]) {
381
+ throw new Error(`Server error: ${resultData["error"]}`);
382
+ }
383
+ /* v8 ignore stop */
384
+ if (resultData["health_check_failure"]) {
385
+ throw new Error(`Health check failure:\n${resultData["health_check_failure"]}`);
386
+ }
387
+ if (resultData["flaky"]) {
388
+ throw new Error(`Flaky test detected: ${resultData["flaky"]}`);
389
+ }
390
+ /* v8 ignore start: server always sends interesting_test_cases */
391
+ const nInteresting = resultData["interesting_test_cases"] ?? 0;
392
+ /* v8 ignore stop */
393
+ // Final replays for interesting test cases
394
+ let finalResult = null;
395
+ for (let i = 0; i < nInteresting; i++) {
396
+ const [eventId, eventPayload] = testStream.receiveRequest();
397
+ const event = decode(eventPayload);
398
+ const streamId = event["stream_id"];
399
+ const testCaseStream = connection.connectStream(streamId);
400
+ testStream.writeReply(eventId, ackNull);
401
+ const ds = new ServerDataSource(connection, testCaseStream);
402
+ const result = runTestCase(ds, this.testFn, true);
403
+ /* v8 ignore start: replay cases are always interesting */
404
+ if (result.status === "interesting") {
405
+ finalResult = result;
406
+ }
407
+ /* v8 ignore stop */
408
+ }
409
+ testStream.close();
410
+ /* v8 ignore start: server always sends passed field */
411
+ const passed = resultData["passed"] ?? true;
412
+ /* v8 ignore stop */
413
+ const testFailed = !passed;
414
+ /* v8 ignore start: only runs inside Antithesis */
415
+ if (isRunningInAntithesis() && this._testLocation) {
416
+ emitAntithesisAssertion(this._testLocation, !testFailed);
417
+ }
418
+ /* v8 ignore stop */
419
+ if (testFailed) {
420
+ let msg = "unknown";
421
+ /* v8 ignore start: finalResult is always set when test fails with interesting cases */
422
+ if (finalResult && finalResult.status === "interesting") {
423
+ const err = finalResult.error;
424
+ msg = err instanceof Error ? err.message : String(err);
425
+ }
426
+ /* v8 ignore stop */
427
+ throw new Error(`Property test failed: ${msg}`);
428
+ }
429
+ }
430
+ }
431
+ /**
432
+ * Wrap a property-based test body into a function suitable for a test runner.
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * import { test } from 'vitest';
437
+ * import * as hegel from 'hegel';
438
+ * import * as gs from 'hegel/generators';
439
+ *
440
+ * test('addition is commutative', hegel.test((tc) => {
441
+ * const x = tc.draw(gs.integers());
442
+ * const y = tc.draw(gs.integers());
443
+ * expect(x + y).toBe(y + x);
444
+ * }));
445
+ * ```
446
+ */
447
+ export function test(testFn, settings) {
448
+ return () => {
449
+ const h = new Hegel(testFn);
450
+ if (settings)
451
+ h.settings(settings);
452
+ h.run();
453
+ };
454
+ }
455
+ //# sourceMappingURL=runner.js.map