@decaf-ts/utils 0.9.6 → 0.10.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.
@@ -1,260 +1,2 @@
1
- import path from "path";
2
- import fs from "fs";
3
- import { installIfNotAvailable } from "./fs.js";
4
- /**
5
- * @description Environment variable key for Jest HTML reporters temporary directory path
6
- * @summary Constant defining the environment variable key for Jest HTML reporters
7
- * @const JestReportersTempPathEnvKey
8
- * @memberOf module:utils
9
- */
10
- export const JestReportersTempPathEnvKey = "JEST_HTML_REPORTERS_TEMP_DIR_PATH";
11
- /**
12
- * @description Array of dependencies required by the test reporter
13
- * @summary List of npm packages needed for reporting functionality
14
- * @const dependencies
15
- * @memberOf module:utils
16
- */
17
- const dependencies = ["jest-html-reporters", "json2md", "chartjs-node-canvas"];
18
- /**
19
- * @description Normalizes imports to handle both CommonJS and ESModule formats
20
- * @summary Utility function to handle module import differences between formats
21
- * @template T - Type of the imported module
22
- * @param {Promise<T>} importPromise - Promise returned by dynamic import
23
- * @return {Promise<T>} Normalized module
24
- * @function normalizeImport
25
- * @memberOf module:utils
26
- */
27
- async function normalizeImport(importPromise) {
28
- // CommonJS's `module.exports` is wrapped as `default` in ESModule.
29
- return importPromise.then((m) => (m.default || m));
30
- }
31
- /**
32
- * @description Test reporting utility class for managing test results and evidence
33
- * @summary A comprehensive test reporter that handles various types of test artifacts including messages,
34
- * attachments, data, images, tables, and graphs. It provides methods to report and store test evidence
35
- * in different formats and manages dependencies for reporting functionality.
36
- *
37
- * @template T - Type of data being reported
38
- * @param {string} [testCase="tests"] - Name of the test case
39
- * @param {string} [basePath] - Base path for storing test reports
40
- * @class
41
- *
42
- * @example
43
- * ```typescript
44
- * const reporter = new TestReporter('login-test');
45
- *
46
- * // Report test messages
47
- * await reporter.reportMessage('Test Started', 'Login flow initiated');
48
- *
49
- * // Report test data
50
- * await reporter.reportData('user-credentials', { username: 'test' }, 'json');
51
- *
52
- * // Report test results table
53
- * await reporter.reportTable('test-results', {
54
- * headers: ['Step', 'Status'],
55
- * rows: [
56
- * { Step: 'Login', Status: 'Pass' },
57
- * { Step: 'Validation', Status: 'Pass' }
58
- * ]
59
- * });
60
- *
61
- * // Report test evidence
62
- * await reporter.reportAttachment('Screenshot', screenshotBuffer);
63
- * ```
64
- *
65
- * @mermaid
66
- * sequenceDiagram
67
- * participant Client
68
- * participant TestReporter
69
- * participant FileSystem
70
- * participant Dependencies
71
- *
72
- * Client->>TestReporter: new TestReporter(testCase, basePath)
73
- * TestReporter->>FileSystem: Create report directory
74
- *
75
- * alt Report Message
76
- * Client->>TestReporter: reportMessage(title, message)
77
- * TestReporter->>Dependencies: Import helpers
78
- * TestReporter->>FileSystem: Store message
79
- * else Report Data
80
- * Client->>TestReporter: reportData(reference, data, type)
81
- * TestReporter->>Dependencies: Process data
82
- * TestReporter->>FileSystem: Store formatted data
83
- * else Report Table
84
- * Client->>TestReporter: reportTable(reference, tableDef)
85
- * TestReporter->>Dependencies: Convert to MD format
86
- * TestReporter->>FileSystem: Store table
87
- * end
88
- */
89
- export class TestReporter {
90
- constructor(testCase = "tests", basePath = path.join(process.cwd(), "workdocs", "reports", "evidences")) {
91
- this.testCase = testCase;
92
- this.basePath = basePath;
93
- this.basePath = path.join(basePath, this.testCase);
94
- if (!fs.existsSync(this.basePath)) {
95
- fs.mkdirSync(basePath, { recursive: true });
96
- }
97
- }
98
- /**
99
- * @description Imports required helper functions
100
- * @summary Ensures all necessary dependencies are available and imports helper functions
101
- * @return {Promise<void>} Promise that resolves when helpers are imported
102
- */
103
- async importHelpers() {
104
- this.deps = await installIfNotAvailable([dependencies[0]], this.deps);
105
- // if (!process.env[JestReportersTempPathEnvKey])
106
- // process.env[JestReportersTempPathEnvKey] = './workdocs/reports';
107
- const { addMsg, addAttach } = await normalizeImport(import(`${dependencies[0]}/helper`));
108
- TestReporter.addMsgFunction = addMsg;
109
- TestReporter.addAttachFunction = addAttach;
110
- }
111
- /**
112
- * @description Reports a message to the test report
113
- * @summary Adds a formatted message to the test report with an optional title
114
- * @param {string} title - Title of the message
115
- * @param {string | object} message - Content of the message
116
- * @return {Promise<void>} Promise that resolves when the message is reported
117
- */
118
- async reportMessage(title, message) {
119
- if (!TestReporter.addMsgFunction)
120
- await this.importHelpers();
121
- const msg = `${title}${message ? `\n${message}` : ""}`;
122
- await TestReporter.addMsgFunction({ message: msg });
123
- }
124
- /**
125
- * @description Reports an attachment to the test report
126
- * @summary Adds a formatted message to the test report with an optional title
127
- * @param {string} title - Title of the message
128
- * @param {string | Buffer} attachment - Content of the message
129
- * @return {Promise<void>} Promise that resolves when the message is reported
130
- */
131
- async reportAttachment(title, attachment) {
132
- if (!TestReporter.addAttachFunction)
133
- await this.importHelpers();
134
- await TestReporter.addAttachFunction({
135
- attach: attachment,
136
- description: title,
137
- });
138
- }
139
- /**
140
- * @description Reports data with specified type
141
- * @summary Processes and stores data in the test report with formatting
142
- * @param {string} reference - Reference identifier for the data
143
- * @param {string | number | object} data - Data to be reported
144
- * @param {PayloadType} type - Type of the payload
145
- * @param {boolean} [trim=false] - Whether to trim the data
146
- * @return {Promise<void>} Promise that resolves when data is reported
147
- */
148
- async report(reference, data, type, trim = false) {
149
- try {
150
- let attachFunction = this.reportMessage.bind(this);
151
- let extension = ".txt";
152
- switch (type) {
153
- case "image":
154
- data = Buffer.from(data);
155
- extension = ".png";
156
- attachFunction = this.reportAttachment.bind(this);
157
- break;
158
- case "json":
159
- if (trim) {
160
- if (data.request)
161
- delete data["request"];
162
- if (data.config)
163
- delete data["config"];
164
- }
165
- data = JSON.stringify(data, null, 2);
166
- extension = ".json";
167
- break;
168
- case "md":
169
- extension = ".md";
170
- break;
171
- case "text":
172
- extension = ".txt";
173
- break;
174
- default:
175
- console.log(`Unsupported type ${type}. assuming text`);
176
- }
177
- reference = reference.includes("\n")
178
- ? reference
179
- : `${reference}${extension}`;
180
- await attachFunction(reference, data);
181
- }
182
- catch (e) {
183
- throw new Error(`Could not store attach artifact ${reference} under to test report ${this.testCase} - ${e}`);
184
- }
185
- }
186
- /**
187
- * @description Reports data with a specified type
188
- * @summary Wrapper method for reporting various types of data
189
- * @param {string} reference - Reference identifier for the data
190
- * @param {string | number | object} data - Data to be reported
191
- * @param {PayloadType} [type="json"] - Type of the payload
192
- * @param {boolean} [trim=false] - Whether to trim the data
193
- * @return {Promise<void>} Promise that resolves when data is reported
194
- */
195
- async reportData(reference, data, type = "json", trim = false) {
196
- return this.report(reference, data, type, trim);
197
- }
198
- /**
199
- * @description Reports a JSON object
200
- * @summary Convenience method for reporting JSON objects
201
- * @param {string} reference - Reference identifier for the object
202
- * @param {object} json - JSON object to be reported
203
- * @param {boolean} [trim=false] - Whether to trim the object
204
- * @return {Promise<void>} Promise that resolves when object is reported
205
- */
206
- async reportObject(reference, json, trim = false) {
207
- return this.report(reference, json, "json", trim);
208
- }
209
- /**
210
- * @description Reports a table in markdown format
211
- * @summary Converts and stores a table definition in markdown format
212
- * @param {string} reference - Reference identifier for the table
213
- * @param {MdTableDefinition} tableDef - Table definition object
214
- * @return {Promise<void>} Promise that resolves when table is reported
215
- */
216
- async reportTable(reference, tableDef) {
217
- this.deps = await installIfNotAvailable([dependencies[1]], this.deps);
218
- let txt;
219
- try {
220
- const json2md = await normalizeImport(import(`${dependencies[1]}`));
221
- txt = json2md(tableDef);
222
- }
223
- catch (e) {
224
- throw new Error(`Could not convert JSON to Markdown - ${e}`);
225
- }
226
- return this.report(reference, txt, "md");
227
- }
228
- /**
229
- * @description Reports a graph using Chart.js
230
- * @summary Generates and stores a graph visualization
231
- * @param {string} reference - Reference identifier for the graph
232
- * @param {any} config - Chart.js configuration object
233
- * @return {Promise<void>} Promise that resolves when graph is reported
234
- */
235
- async reportGraph(reference, config) {
236
- this.deps = await installIfNotAvailable([dependencies[2]], this.deps);
237
- const { ChartJSNodeCanvas } = await normalizeImport(import(dependencies[2]));
238
- const width = 600; //px
239
- const height = 800; //px
240
- const backgroundColour = "white"; // Uses https://www.w3schools.com/tags/canvas_fillstyle.asp
241
- const chartJSNodeCanvas = new ChartJSNodeCanvas({
242
- width,
243
- height,
244
- backgroundColour,
245
- });
246
- const image = await chartJSNodeCanvas.renderToBuffer(config);
247
- return await this.reportImage(reference, image);
248
- }
249
- /**
250
- * @description Reports an image to the test report
251
- * @summary Stores an image buffer in the test report
252
- * @param {string} reference - Reference identifier for the image
253
- * @param {Buffer} buffer - Image data buffer
254
- * @return {Promise<void>} Promise that resolves when image is reported
255
- */
256
- async reportImage(reference, buffer) {
257
- return this.report(reference, buffer, "image");
258
- }
259
- }
1
+ export * from "./../tests/index.js";
260
2
  //# sourceMappingURL=tests.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tests.js","sourceRoot":"","sources":["../../../src/utils/tests.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tests.js","sourceRoot":"","sources":["../../../src/utils/tests.ts"],"names":[],"mappings":"AAAA,oCAAyB"}
package/lib/index.cjs CHANGED
@@ -43,7 +43,7 @@ __exportStar(require("./writers/index.cjs"), exports);
43
43
  * @const VERSION
44
44
  * @memberOf module:utils
45
45
  */
46
- exports.VERSION = "0.9.5";
46
+ exports.VERSION = "0.10.0";
47
47
  /**
48
48
  * @description Represents the current version of the module.
49
49
  * @summary Stores the version for the @decaf-ts/utils package. The build replaces
package/lib/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export * from "./writers";
26
26
  * @const VERSION
27
27
  * @memberOf module:utils
28
28
  */
29
- export declare const VERSION = "0.9.5";
29
+ export declare const VERSION = "0.10.0";
30
30
  /**
31
31
  * @description Represents the current version of the module.
32
32
  * @summary Stores the version for the @decaf-ts/utils package. The build replaces
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConsumerRunner = exports.reportingComparer = exports.defaultComparer = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_path_1 = require("node:path");
6
+ const logging_1 = require("@decaf-ts/logging");
7
+ const TestReporter_1 = require("./TestReporter.cjs");
8
+ const parseData = (data) => {
9
+ const [timestamp, , child, action] = data.split(" - ");
10
+ return {
11
+ timestamp: parseInt(timestamp, 10),
12
+ child,
13
+ action,
14
+ };
15
+ };
16
+ const defaultComparer = async (consumerData, producerData) => {
17
+ const sortedConsumerData = Object.keys(consumerData)
18
+ .reduce((accum, key) => {
19
+ const identifier = Number(key);
20
+ const entries = consumerData[identifier] ?? [];
21
+ accum.push(...entries.map((entry) => parseData(entry)));
22
+ return accum;
23
+ }, [])
24
+ .sort((a, b) => a.timestamp - b.timestamp);
25
+ const sortedProducerData = Object.keys(producerData)
26
+ .reduce((accum, key) => {
27
+ const identifier = Number(key);
28
+ const entries = producerData[identifier] ?? [];
29
+ accum.push(...entries.map((entry) => parseData(entry)));
30
+ return accum;
31
+ }, [])
32
+ .sort((a, b) => a.timestamp - b.timestamp);
33
+ if (sortedProducerData.length !== sortedConsumerData.length) {
34
+ throw new Error("Producer data and consumer data does not match in length");
35
+ }
36
+ let counter = -1;
37
+ const isMatching = sortedProducerData.every((producer, index) => {
38
+ counter = index;
39
+ const consumer = sortedConsumerData[index];
40
+ return (producer.child === consumer.child && producer.action === consumer.action);
41
+ });
42
+ if (!isMatching) {
43
+ const errorLines = [
44
+ `Producer data and consumer data do not sort the same way as of record ${counter}:`,
45
+ " | CONSUMER | PRODUCER |",
46
+ " | id | action | timestamp | id | action | timestamp |",
47
+ ];
48
+ sortedProducerData.forEach((producer, index) => {
49
+ if (index < counter || index > counter + 15) {
50
+ return;
51
+ }
52
+ const consumer = sortedConsumerData[index];
53
+ errorLines.push(` ${index < 10 ? `0${index}` : index}| ${consumer.child} | ${consumer.action} | ${consumer.timestamp} | ${producer.child} | ${producer.action} | ${producer.timestamp} |`);
54
+ });
55
+ throw new Error(errorLines.join("\n"));
56
+ }
57
+ return {
58
+ consumer: sortedConsumerData,
59
+ producer: sortedProducerData,
60
+ };
61
+ };
62
+ exports.defaultComparer = defaultComparer;
63
+ const formatTimestamp = (value) => new Date(value).toISOString();
64
+ const reportingComparer = async (consumerData, producerData, options) => {
65
+ const reporter = options?.reporter ??
66
+ new TestReporter_1.TestReporter(options?.testCase ?? "consumer-producer");
67
+ const referencePrefix = options?.referencePrefix ?? "consumer-producer";
68
+ try {
69
+ const comparison = await (0, exports.defaultComparer)(consumerData, producerData);
70
+ const rows = comparison.consumer.map((consumerEntry, index) => {
71
+ const producerEntry = comparison.producer[index];
72
+ return {
73
+ Index: `${index}`,
74
+ "Consumer Child": consumerEntry.child,
75
+ "Consumer Action": consumerEntry.action,
76
+ "Consumer Timestamp": formatTimestamp(consumerEntry.timestamp),
77
+ "Producer Child": producerEntry?.child ?? "N/A",
78
+ "Producer Action": producerEntry?.action ?? "N/A",
79
+ "Producer Timestamp": producerEntry
80
+ ? formatTimestamp(producerEntry.timestamp)
81
+ : "N/A",
82
+ };
83
+ });
84
+ await Promise.allSettled([
85
+ reporter.reportMessage(`${referencePrefix}-comparison`, `Consumer and producer logs matched (${comparison.consumer.length} entries).`),
86
+ reporter.reportTable(`${referencePrefix}-logs`, {
87
+ headers: [
88
+ "Index",
89
+ "Consumer Child",
90
+ "Consumer Action",
91
+ "Consumer Timestamp",
92
+ "Producer Child",
93
+ "Producer Action",
94
+ "Producer Timestamp",
95
+ ],
96
+ rows,
97
+ }),
98
+ ]);
99
+ return comparison;
100
+ }
101
+ catch (error) {
102
+ const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
103
+ await Promise.allSettled([
104
+ reporter.reportMessage(`${referencePrefix}-mismatch`, message),
105
+ reporter.reportObject(`${referencePrefix}-consumer`, consumerData),
106
+ reporter.reportObject(`${referencePrefix}-producer`, producerData),
107
+ ]);
108
+ throw error;
109
+ }
110
+ };
111
+ exports.reportingComparer = reportingComparer;
112
+ class ConsumerRunner extends logging_1.LoggedClass {
113
+ constructor(action, consumerHandler, compareHandler) {
114
+ super();
115
+ this.forkedCache = [];
116
+ this.consumerResults = {};
117
+ this.producerResults = {};
118
+ this.childExitPromises = [];
119
+ this.completionTriggered = false;
120
+ this.activeHandlers = 0;
121
+ this.action = action;
122
+ this.handler = consumerHandler;
123
+ this.comparerHandle = compareHandler ?? exports.defaultComparer;
124
+ this.reset();
125
+ }
126
+ reset() {
127
+ this.forkedCache = [];
128
+ this.consumerResults = {};
129
+ this.producerResults = {};
130
+ this.completionTriggered = false;
131
+ this.childExitPromises = [];
132
+ this.activeHandlers = 0;
133
+ }
134
+ waitForChildExit() {
135
+ if (!this.childExitPromises?.length) {
136
+ return Promise.resolve();
137
+ }
138
+ const exits = [...this.childExitPromises];
139
+ this.childExitPromises = [];
140
+ return Promise.allSettled(exits).then(() => void 0);
141
+ }
142
+ store(identifier, action, timeout, times, count, random) {
143
+ const logParts = [
144
+ Date.now(),
145
+ "PRODUCER",
146
+ identifier,
147
+ action,
148
+ ];
149
+ if (timeout) {
150
+ logParts.push(timeout);
151
+ }
152
+ if (times && count) {
153
+ logParts.push(`${count}/${times}`, random ?? false);
154
+ }
155
+ const log = logParts.join(" - ");
156
+ if (!this.producerResults[identifier]) {
157
+ this.producerResults[identifier] = [];
158
+ }
159
+ this.producerResults[identifier].push(log);
160
+ }
161
+ recordConsumer(identifier) {
162
+ const logParts = [
163
+ Date.now(),
164
+ "CONSUMER",
165
+ identifier,
166
+ this.action,
167
+ ];
168
+ const log = logParts.join(" - ");
169
+ if (!this.consumerResults[identifier]) {
170
+ this.consumerResults[identifier] = [];
171
+ }
172
+ this.consumerResults[identifier].push(log);
173
+ }
174
+ isProducerComplete(count, times) {
175
+ const producerKeys = Object.keys(this.producerResults);
176
+ if (producerKeys.length !== count) {
177
+ return false;
178
+ }
179
+ return producerKeys.every((key) => this.producerResults[Number(key)]?.length === times);
180
+ }
181
+ isConsumerComplete(count, times) {
182
+ const consumerKeys = Object.keys(this.consumerResults);
183
+ if (consumerKeys.length !== count) {
184
+ return false;
185
+ }
186
+ return consumerKeys.every((key) => this.consumerResults[Number(key)]?.length === times);
187
+ }
188
+ terminateChildren(forceKill = false) {
189
+ if (!this.forkedCache) {
190
+ return this.waitForChildExit();
191
+ }
192
+ const cached = this.forkedCache;
193
+ this.forkedCache = undefined;
194
+ cached.forEach((forked, index) => {
195
+ if (!forked.connected && !forceKill) {
196
+ return;
197
+ }
198
+ try {
199
+ forked.send({
200
+ identifier: index,
201
+ terminate: true,
202
+ });
203
+ }
204
+ catch {
205
+ // IPC channel already closed; nothing else to do.
206
+ }
207
+ if (forceKill && !forked.killed) {
208
+ forked.kill();
209
+ }
210
+ });
211
+ return this.waitForChildExit();
212
+ }
213
+ async run(count, timeout, times, random) {
214
+ this.reset();
215
+ const childPath = (0, node_path_1.join)(__dirname, "ProducerChildProcess.cjs");
216
+ return new Promise((resolve, reject) => {
217
+ const snapshotState = () => {
218
+ const summarize = (records) => Object.keys(records).reduce((acc, key) => {
219
+ acc[key] = records[Number(key)]?.length ?? 0;
220
+ return acc;
221
+ }, {});
222
+ return {
223
+ producers: summarize(this.producerResults),
224
+ consumers: summarize(this.consumerResults),
225
+ activeHandlers: this.activeHandlers,
226
+ };
227
+ };
228
+ const handleError = (error) => {
229
+ if (this.completionTriggered) {
230
+ return;
231
+ }
232
+ this.completionTriggered = true;
233
+ Promise.resolve(this.terminateChildren(true)).finally(() => reject(error));
234
+ };
235
+ const finalizeIfComplete = () => {
236
+ if (this.completionTriggered) {
237
+ return;
238
+ }
239
+ if (!this.isProducerComplete(count, times) ||
240
+ !this.isConsumerComplete(count, times) ||
241
+ this.activeHandlers > 0) {
242
+ return;
243
+ }
244
+ this.completionTriggered = true;
245
+ if (process.env.DEBUG_CONSUMER_RUNNER === "1") {
246
+ console.debug("ConsumerRunner finalize state", snapshotState());
247
+ }
248
+ try {
249
+ const comparisonPromise = Promise.resolve(this.comparerHandle(this.consumerResults, this.producerResults));
250
+ Promise.all([comparisonPromise, this.waitForChildExit()])
251
+ .then(async ([comparison]) => {
252
+ await new Promise((resolveDelay) => setImmediate(resolveDelay));
253
+ resolve(comparison);
254
+ })
255
+ .catch(reject);
256
+ }
257
+ catch (error) {
258
+ reject(error);
259
+ }
260
+ };
261
+ for (let identifier = 1; identifier < count + 1; identifier += 1) {
262
+ const forked = (0, node_child_process_1.fork)(childPath);
263
+ this.forkedCache?.push(forked);
264
+ this.childExitPromises?.push(new Promise((resolveChild) => {
265
+ forked.once("exit", () => resolveChild());
266
+ }));
267
+ forked.on("error", handleError);
268
+ forked.on("message", async (message) => {
269
+ if (this.completionTriggered) {
270
+ return;
271
+ }
272
+ const { identifier: childId, args, action, timeout: childTimeout, times: childTimes, random: childRandom, } = message;
273
+ this.activeHandlers += 1;
274
+ let handlerFailed = false;
275
+ if (process.env.DEBUG_CONSUMER_RUNNER === "1") {
276
+ console.debug("ConsumerRunner message:start", {
277
+ childId,
278
+ producerCount: this.producerResults[childId]?.length ?? 0,
279
+ consumerCount: this.consumerResults[childId]?.length ?? 0,
280
+ activeHandlers: this.activeHandlers,
281
+ });
282
+ }
283
+ try {
284
+ this.store(childId, action, childTimeout, childTimes, count, childRandom);
285
+ const handlerArgs = Array.isArray(args) ? args : [];
286
+ await Promise.resolve(this.handler(childId, ...handlerArgs));
287
+ this.recordConsumer(childId);
288
+ if (process.env.DEBUG_CONSUMER_RUNNER === "1") {
289
+ console.debug("ConsumerRunner message:complete", {
290
+ childId,
291
+ producerCount: this.producerResults[childId]?.length ?? 0,
292
+ consumerCount: this.consumerResults[childId]?.length ?? 0,
293
+ activeHandlers: this.activeHandlers,
294
+ });
295
+ }
296
+ }
297
+ catch (error) {
298
+ handlerFailed = true;
299
+ handleError(error);
300
+ }
301
+ finally {
302
+ this.activeHandlers = Math.max(0, this.activeHandlers - 1);
303
+ if (!handlerFailed) {
304
+ finalizeIfComplete();
305
+ }
306
+ }
307
+ });
308
+ }
309
+ this.forkedCache?.forEach((forked, index) => {
310
+ forked.send({
311
+ identifier: index,
312
+ action: this.action,
313
+ timeout,
314
+ times,
315
+ random,
316
+ });
317
+ });
318
+ });
319
+ }
320
+ }
321
+ exports.ConsumerRunner = ConsumerRunner;
322
+ //# sourceMappingURL=Consumer.js.map
@@ -0,0 +1,42 @@
1
+ import { LoggedClass } from "@decaf-ts/logging";
2
+ import { TestReporter } from "./TestReporter";
3
+ type LogStore = Record<number, string[]>;
4
+ export interface ParsedLog {
5
+ timestamp: number;
6
+ child: string;
7
+ action: string;
8
+ }
9
+ export interface ComparerResult {
10
+ consumer: ParsedLog[];
11
+ producer: ParsedLog[];
12
+ }
13
+ type Comparer = (consumerData: LogStore, producerData: LogStore) => Promise<ComparerResult>;
14
+ type ConsumerHandler = (identifier: number, ...args: unknown[]) => unknown | Promise<unknown>;
15
+ export declare const defaultComparer: Comparer;
16
+ export interface ReportingComparerOptions {
17
+ reporter?: TestReporter;
18
+ testCase?: string;
19
+ referencePrefix?: string;
20
+ }
21
+ export declare const reportingComparer: (consumerData: LogStore, producerData: LogStore, options?: ReportingComparerOptions) => Promise<ComparerResult>;
22
+ export declare class ConsumerRunner extends LoggedClass {
23
+ private readonly action;
24
+ private readonly handler;
25
+ private readonly comparerHandle;
26
+ private forkedCache;
27
+ private consumerResults;
28
+ private producerResults;
29
+ private childExitPromises;
30
+ private completionTriggered;
31
+ private activeHandlers;
32
+ constructor(action: string, consumerHandler: ConsumerHandler, compareHandler?: Comparer);
33
+ private reset;
34
+ private waitForChildExit;
35
+ private store;
36
+ private recordConsumer;
37
+ private isProducerComplete;
38
+ private isConsumerComplete;
39
+ private terminateChildren;
40
+ run(count: number, timeout: number | undefined, times: number, random: boolean | undefined): Promise<ComparerResult>;
41
+ }
42
+ export {};