@forgehive/task 0.2.3 → 0.2.4
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.
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/dist/test/add-listener-with-boundaries.test.js +78 -7
- package/dist/test/add-listener-with-boundaries.test.js.map +1 -1
- package/dist/test/add-listener.test.js +36 -0
- package/dist/test/add-listener.test.js.map +1 -1
- package/dist/test/boundary-modes.test.js +45 -5
- package/dist/test/boundary-modes.test.js.map +1 -1
- package/dist/test/execution-record-boundaries.test.js +12 -2
- package/dist/test/execution-record-boundaries.test.js.map +1 -1
- package/dist/test/integration-enhanced-records.test.d.ts +2 -0
- package/dist/test/integration-enhanced-records.test.d.ts.map +1 -0
- package/dist/test/integration-enhanced-records.test.js +467 -0
- package/dist/test/integration-enhanced-records.test.js.map +1 -0
- package/dist/test/metrics-collection.test.d.ts +2 -0
- package/dist/test/metrics-collection.test.d.ts.map +1 -0
- package/dist/test/metrics-collection.test.js +409 -0
- package/dist/test/metrics-collection.test.js.map +1 -0
- package/dist/test/performance-edge-cases.test.d.ts +2 -0
- package/dist/test/performance-edge-cases.test.d.ts.map +1 -0
- package/dist/test/performance-edge-cases.test.js +502 -0
- package/dist/test/performance-edge-cases.test.js.map +1 -0
- package/dist/test/run-boundary.test.js +27 -3
- package/dist/test/run-boundary.test.js.map +1 -1
- package/dist/test/safe-replay-complex-boundary.test.js +110 -9
- package/dist/test/safe-replay-complex-boundary.test.js.map +1 -1
- package/dist/test/safe-replay.test.js +35 -5
- package/dist/test/safe-replay.test.js.map +1 -1
- package/dist/test/safe-run.test.js +46 -4
- package/dist/test/safe-run.test.js.map +1 -1
- package/dist/test/setmetrics-boundary.test.d.ts +2 -0
- package/dist/test/setmetrics-boundary.test.d.ts.map +1 -0
- package/dist/test/setmetrics-boundary.test.js +195 -0
- package/dist/test/setmetrics-boundary.test.js.map +1 -0
- package/dist/test/task-with-boundaries.test.js +63 -2
- package/dist/test/task-with-boundaries.test.js.map +1 -1
- package/dist/test/timing-capture.test.d.ts +2 -0
- package/dist/test/timing-capture.test.d.ts.map +1 -0
- package/dist/test/timing-capture.test.js +304 -0
- package/dist/test/timing-capture.test.js.map +1 -0
- package/dist/test/timing-utilities.test.d.ts +2 -0
- package/dist/test/timing-utilities.test.d.ts.map +1 -0
- package/dist/test/timing-utilities.test.js +127 -0
- package/dist/test/timing-utilities.test.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +78 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/boundary.d.ts +3 -0
- package/dist/utils/boundary.d.ts.map +1 -1
- package/dist/utils/boundary.js +11 -2
- package/dist/utils/boundary.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +63 -8
- package/src/test/add-listener-with-boundaries.test.ts +78 -7
- package/src/test/add-listener.test.ts +36 -0
- package/src/test/boundary-modes.test.ts +45 -5
- package/src/test/execution-record-boundaries.test.ts +12 -2
- package/src/test/metrics-collection.test.ts +476 -0
- package/src/test/performance-edge-cases.test.ts +596 -0
- package/src/test/run-boundary.test.ts +27 -3
- package/src/test/safe-replay-complex-boundary.test.ts +115 -10
- package/src/test/safe-replay.test.ts +35 -5
- package/src/test/safe-run.test.ts +46 -4
- package/src/test/setmetrics-boundary.test.ts +223 -0
- package/src/test/task-with-boundaries.test.ts +71 -5
- package/src/test/timing-capture.test.ts +348 -0
- package/src/test/timing-utilities.test.ts +145 -0
- package/src/types.ts +139 -0
- package/src/utils/boundary.ts +15 -2
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { createTask, TimingTracker, validateMetric, createMetric } from '../index'
|
|
3
|
+
import { Schema } from '@forgehive/schema'
|
|
4
|
+
|
|
5
|
+
describe('Performance and Edge Case Tests', () => {
|
|
6
|
+
describe('Performance impact of timing and metrics collection', () => {
|
|
7
|
+
it('should have minimal performance overhead for timing collection', async () => {
|
|
8
|
+
const baselineTask = createTask({
|
|
9
|
+
name: 'baseline-performance',
|
|
10
|
+
schema: new Schema({ iterations: Schema.number() }),
|
|
11
|
+
boundaries: {
|
|
12
|
+
simpleOperation: async (value: number) => {
|
|
13
|
+
return value * 2
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
fn: async ({ iterations }, { simpleOperation }) => {
|
|
17
|
+
const results = []
|
|
18
|
+
for (let i = 0; i < iterations; i++) {
|
|
19
|
+
results.push(await simpleOperation(i))
|
|
20
|
+
}
|
|
21
|
+
return { results }
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const timingTask = createTask({
|
|
26
|
+
name: 'timing-performance',
|
|
27
|
+
schema: new Schema({ iterations: Schema.number() }),
|
|
28
|
+
boundaries: {
|
|
29
|
+
simpleOperation: async (value: number) => {
|
|
30
|
+
return value * 2
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
fn: async ({ iterations }, { simpleOperation, setMetrics }) => {
|
|
34
|
+
const results = []
|
|
35
|
+
for (let i = 0; i < iterations; i++) {
|
|
36
|
+
results.push(await simpleOperation(i))
|
|
37
|
+
// Add timing metrics for each operation
|
|
38
|
+
await setMetrics({ type: 'performance', name: 'operation_completed', value: i })
|
|
39
|
+
}
|
|
40
|
+
return { results }
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const iterations = 100
|
|
45
|
+
|
|
46
|
+
// Measure baseline performance (without extensive metrics)
|
|
47
|
+
const baselineStart = Date.now()
|
|
48
|
+
const [baselineResult, baselineError, baselineRecord] = await baselineTask.safeRun({ iterations })
|
|
49
|
+
const baselineTime = Date.now() - baselineStart
|
|
50
|
+
|
|
51
|
+
expect(baselineError).toBeNull()
|
|
52
|
+
expect(baselineResult?.results).toHaveLength(iterations)
|
|
53
|
+
|
|
54
|
+
// Measure performance with timing and metrics
|
|
55
|
+
const timingStart = Date.now()
|
|
56
|
+
const [timingResult, timingError, timingRecord] = await timingTask.safeRun({ iterations })
|
|
57
|
+
const timingTime = Date.now() - timingStart
|
|
58
|
+
|
|
59
|
+
expect(timingError).toBeNull()
|
|
60
|
+
expect(timingResult?.results).toHaveLength(iterations)
|
|
61
|
+
expect(timingRecord.metrics).toHaveLength(iterations)
|
|
62
|
+
|
|
63
|
+
// Performance overhead should be reasonable (less than 50% increase)
|
|
64
|
+
const overhead = (timingTime - baselineTime) / baselineTime
|
|
65
|
+
expect(overhead).toBeLessThan(0.5) // Less than 50% overhead
|
|
66
|
+
|
|
67
|
+
// Verify timing accuracy - duration should be 0 or greater for fast operations
|
|
68
|
+
expect(baselineRecord.timing?.duration).toBeGreaterThanOrEqual(0)
|
|
69
|
+
expect(timingRecord.timing?.duration).toBeGreaterThanOrEqual(0)
|
|
70
|
+
|
|
71
|
+
console.log('Performance Test Results:')
|
|
72
|
+
console.log(`Baseline: ${baselineTime}ms, With timing: ${timingTime}ms`)
|
|
73
|
+
console.log(`Overhead: ${(overhead * 100).toFixed(1)}%`)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should handle high-frequency timing operations efficiently', () => {
|
|
77
|
+
const timings: Array<{ startTime: number; endTime: number; duration?: number }> = []
|
|
78
|
+
const iterations = 1000
|
|
79
|
+
|
|
80
|
+
const start = Date.now()
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < iterations; i++) {
|
|
83
|
+
const tracker = TimingTracker.create()
|
|
84
|
+
tracker.start()
|
|
85
|
+
|
|
86
|
+
// Simulate very brief operation
|
|
87
|
+
for (let j = 0; j < 10; j++) {
|
|
88
|
+
Math.random()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const timing = tracker.end()
|
|
92
|
+
if (timing) {
|
|
93
|
+
timings.push(timing)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const totalTime = Date.now() - start
|
|
98
|
+
|
|
99
|
+
// Verify all timings were captured
|
|
100
|
+
expect(timings).toHaveLength(iterations)
|
|
101
|
+
timings.forEach(timing => {
|
|
102
|
+
expect(timing).not.toBeNull()
|
|
103
|
+
expect(timing.duration).toBeGreaterThanOrEqual(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// High-frequency timing should complete reasonably quickly
|
|
107
|
+
expect(totalTime).toBeLessThan(1000) // Less than 1 second for 1000 operations
|
|
108
|
+
|
|
109
|
+
console.log(`High-frequency timing: ${iterations} operations in ${totalTime}ms`)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should handle large metrics collections without memory issues', async () => {
|
|
113
|
+
const largeMetricsTask = createTask({
|
|
114
|
+
name: 'large-metrics-test',
|
|
115
|
+
schema: new Schema({ metricCount: Schema.number() }),
|
|
116
|
+
boundaries: {},
|
|
117
|
+
fn: async ({ metricCount }, { setMetrics }) => {
|
|
118
|
+
for (let i = 0; i < metricCount; i++) {
|
|
119
|
+
await setMetrics({
|
|
120
|
+
type: i % 3 === 0 ? 'business' : i % 3 === 1 ? 'performance' : 'error',
|
|
121
|
+
name: `metric_${i}`,
|
|
122
|
+
value: Math.random() * 1000
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
return { metricsCreated: metricCount }
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const metricCount = 1000
|
|
130
|
+
const memoryBefore = process.memoryUsage()
|
|
131
|
+
|
|
132
|
+
const [result, error, record] = await largeMetricsTask.safeRun({ metricCount })
|
|
133
|
+
|
|
134
|
+
const memoryAfter = process.memoryUsage()
|
|
135
|
+
|
|
136
|
+
expect(error).toBeNull()
|
|
137
|
+
expect(result?.metricsCreated).toBe(metricCount)
|
|
138
|
+
expect(record.metrics).toHaveLength(metricCount)
|
|
139
|
+
|
|
140
|
+
// Verify metrics are correctly structured
|
|
141
|
+
record.metrics?.forEach((metric, index) => {
|
|
142
|
+
expect(metric).toEqual(expect.objectContaining({
|
|
143
|
+
type: expect.stringMatching(/^(business|performance|error)$/),
|
|
144
|
+
name: `metric_${index}`,
|
|
145
|
+
value: expect.any(Number)
|
|
146
|
+
}))
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Memory usage should not explode
|
|
150
|
+
const memoryIncrease = memoryAfter.heapUsed - memoryBefore.heapUsed
|
|
151
|
+
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024) // Less than 50MB increase
|
|
152
|
+
|
|
153
|
+
console.log(`Large metrics test: ${metricCount} metrics, memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(1)}MB`)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('Edge cases and error handling', () => {
|
|
158
|
+
it('should handle missing timing data gracefully', async () => {
|
|
159
|
+
// Manually create a broken timing scenario
|
|
160
|
+
const tracker = TimingTracker.create()
|
|
161
|
+
// Don't call start()
|
|
162
|
+
const timing = tracker.end()
|
|
163
|
+
|
|
164
|
+
expect(timing).toBeNull()
|
|
165
|
+
|
|
166
|
+
// Test with multiple end calls
|
|
167
|
+
tracker.start()
|
|
168
|
+
const timing1 = tracker.end()
|
|
169
|
+
const timing2 = tracker.end() // Second end call
|
|
170
|
+
|
|
171
|
+
expect(timing1).not.toBeNull()
|
|
172
|
+
expect(timing2).toBeNull() // Should return null on second call
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should handle invalid metrics gracefully', () => {
|
|
176
|
+
// Test boundary conditions for metric validation
|
|
177
|
+
expect(validateMetric({ type: 'business', name: 'test', value: 0 })).toBe(true)
|
|
178
|
+
expect(validateMetric({ type: 'business', name: 'test', value: -1 })).toBe(true)
|
|
179
|
+
expect(validateMetric({ type: 'business', name: 'test', value: 999999 })).toBe(true)
|
|
180
|
+
|
|
181
|
+
// Test invalid cases
|
|
182
|
+
expect(validateMetric({ type: 'business', name: 'test', value: null })).toBe(false)
|
|
183
|
+
expect(validateMetric({ type: 'business', name: 'test', value: undefined })).toBe(false)
|
|
184
|
+
expect(validateMetric({ type: 'business', name: 'test', value: 'string' })).toBe(false)
|
|
185
|
+
expect(validateMetric({ type: 'business', name: 'test', value: {} })).toBe(false)
|
|
186
|
+
expect(validateMetric({ type: 'business', name: 'test', value: [] })).toBe(false)
|
|
187
|
+
|
|
188
|
+
// Test edge cases with createMetric
|
|
189
|
+
expect(() => createMetric('business', 'test', 0)).not.toThrow()
|
|
190
|
+
expect(() => createMetric('business', 'test', -999)).not.toThrow()
|
|
191
|
+
expect(() => createMetric('business', 'test', 0.0001)).not.toThrow()
|
|
192
|
+
|
|
193
|
+
expect(() => createMetric('', 'test', 1)).toThrow()
|
|
194
|
+
expect(() => createMetric('business', '', 1)).toThrow()
|
|
195
|
+
expect(() => createMetric('business', 'test', NaN)).toThrow()
|
|
196
|
+
expect(() => createMetric('business', 'test', Infinity)).toThrow()
|
|
197
|
+
expect(() => createMetric('business', 'test', -Infinity)).toThrow()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should handle memory cleanup and long-running tasks', async () => {
|
|
201
|
+
const longRunningTask = createTask({
|
|
202
|
+
name: 'memory-cleanup-test',
|
|
203
|
+
schema: new Schema({ duration: Schema.number() }),
|
|
204
|
+
boundaries: {
|
|
205
|
+
memoryIntensiveOperation: async (iterations: number) => {
|
|
206
|
+
const data = []
|
|
207
|
+
for (let i = 0; i < iterations; i++) {
|
|
208
|
+
data.push(new Array(1000).fill(Math.random()))
|
|
209
|
+
}
|
|
210
|
+
return { processed: data.length, size: data.length * 1000 }
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
fn: async ({ duration }, { memoryIntensiveOperation, setMetrics }) => {
|
|
214
|
+
const startTime = Date.now()
|
|
215
|
+
const results = []
|
|
216
|
+
|
|
217
|
+
while (Date.now() - startTime < duration) {
|
|
218
|
+
const result = await memoryIntensiveOperation(100)
|
|
219
|
+
results.push(result)
|
|
220
|
+
|
|
221
|
+
await setMetrics({ type: 'performance', name: 'memory_operations', value: 1 })
|
|
222
|
+
await setMetrics({ type: 'business', name: 'data_processed', value: result.processed })
|
|
223
|
+
|
|
224
|
+
// Force garbage collection opportunity
|
|
225
|
+
if (results.length % 10 === 0) {
|
|
226
|
+
if (global.gc) {
|
|
227
|
+
global.gc()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { operations: results.length, totalProcessed: results.reduce((sum, r) => sum + r.processed, 0) }
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const memoryBefore = process.memoryUsage()
|
|
237
|
+
|
|
238
|
+
const [result, error, record] = await longRunningTask.safeRun({ duration: 500 }) // 500ms
|
|
239
|
+
|
|
240
|
+
// Allow some time for garbage collection
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
242
|
+
if (global.gc) {
|
|
243
|
+
global.gc()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const memoryAfter = process.memoryUsage()
|
|
247
|
+
|
|
248
|
+
expect(error).toBeNull()
|
|
249
|
+
expect(result?.operations).toBeGreaterThan(0)
|
|
250
|
+
expect(record.metrics?.length).toBeGreaterThan(0)
|
|
251
|
+
|
|
252
|
+
// Memory should not grow excessively
|
|
253
|
+
const memoryIncrease = memoryAfter.heapUsed - memoryBefore.heapUsed
|
|
254
|
+
expect(memoryIncrease).toBeLessThan(100 * 1024 * 1024) // Less than 100MB
|
|
255
|
+
|
|
256
|
+
console.log(`Memory cleanup test: ${result?.operations} operations, memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(1)}MB`)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('should handle concurrent task executions without interference', async () => {
|
|
260
|
+
const concurrentTask = createTask({
|
|
261
|
+
name: 'concurrent-test',
|
|
262
|
+
schema: new Schema({ taskId: Schema.string(), delay: Schema.number() }),
|
|
263
|
+
boundaries: {
|
|
264
|
+
processWithDelay: async (taskId: string, delay: number) => {
|
|
265
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
266
|
+
return { taskId, processedAt: Date.now() }
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
fn: async ({ taskId, delay }, { processWithDelay, setMetrics, setMetadata }) => {
|
|
270
|
+
await setMetadata('taskId', taskId)
|
|
271
|
+
await setMetadata('concurrencyTest', 'true')
|
|
272
|
+
|
|
273
|
+
await setMetrics({ type: 'business', name: 'concurrent_tasks', value: 1 })
|
|
274
|
+
|
|
275
|
+
const result = await processWithDelay(taskId, delay)
|
|
276
|
+
|
|
277
|
+
await setMetrics({ type: 'performance', name: 'task_delay', value: delay })
|
|
278
|
+
await setMetrics({ type: 'business', name: 'tasks_completed', value: 1 })
|
|
279
|
+
|
|
280
|
+
return { taskId, result, delay }
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Launch multiple concurrent tasks
|
|
285
|
+
const tasks = [
|
|
286
|
+
concurrentTask.safeRun({ taskId: 'task-1', delay: 100 }),
|
|
287
|
+
concurrentTask.safeRun({ taskId: 'task-2', delay: 150 }),
|
|
288
|
+
concurrentTask.safeRun({ taskId: 'task-3', delay: 80 }),
|
|
289
|
+
concurrentTask.safeRun({ taskId: 'task-4', delay: 200 }),
|
|
290
|
+
concurrentTask.safeRun({ taskId: 'task-5', delay: 120 })
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
const results = await Promise.all(tasks)
|
|
294
|
+
|
|
295
|
+
// Verify all tasks completed successfully
|
|
296
|
+
results.forEach(([result, error, record], index) => {
|
|
297
|
+
expect(error).toBeNull()
|
|
298
|
+
expect(result?.taskId).toBe(`task-${index + 1}`)
|
|
299
|
+
expect(record.metrics).toHaveLength(3)
|
|
300
|
+
expect(record.timing).toBeDefined()
|
|
301
|
+
expect(record.boundaries.processWithDelay).toHaveLength(1)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// Verify timing isolation between concurrent tasks
|
|
305
|
+
const timings = results.map(([,, record]) => record.timing!)
|
|
306
|
+
timings.forEach((timing, index) => {
|
|
307
|
+
expect(timing.duration).toBeGreaterThanOrEqual([100, 150, 80, 200, 120][index] - 20)
|
|
308
|
+
expect(timing.duration).toBeLessThan([100, 150, 80, 200, 120][index] + 50)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
console.log(`Concurrent tasks completed: ${results.length}`)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should handle extreme boundary call counts', async () => {
|
|
315
|
+
const extremeBoundaryTask = createTask({
|
|
316
|
+
name: 'extreme-boundary-test',
|
|
317
|
+
schema: new Schema({ callCount: Schema.number() }),
|
|
318
|
+
boundaries: {
|
|
319
|
+
lightweightOperation: async (index: number) => {
|
|
320
|
+
return { index, doubled: index * 2 }
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
fn: async ({ callCount }, { lightweightOperation, setMetrics }) => {
|
|
324
|
+
await setMetrics({ type: 'business', name: 'boundary_test_runs', value: 1 })
|
|
325
|
+
|
|
326
|
+
const results = []
|
|
327
|
+
for (let i = 0; i < callCount; i++) {
|
|
328
|
+
const result = await lightweightOperation(i)
|
|
329
|
+
results.push(result)
|
|
330
|
+
|
|
331
|
+
// Add metrics every 100 calls to avoid excessive metrics
|
|
332
|
+
if (i % 100 === 0) {
|
|
333
|
+
await setMetrics({ type: 'performance', name: 'batch_progress', value: i })
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await setMetrics({ type: 'business', name: 'total_boundary_calls', value: callCount })
|
|
338
|
+
return { results: results.length, lastResult: results[results.length - 1] }
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const callCount = 1000
|
|
343
|
+
const start = Date.now()
|
|
344
|
+
|
|
345
|
+
const [result, error, record] = await extremeBoundaryTask.safeRun({ callCount })
|
|
346
|
+
|
|
347
|
+
const executionTime = Date.now() - start
|
|
348
|
+
|
|
349
|
+
expect(error).toBeNull()
|
|
350
|
+
expect(result?.results).toBe(callCount)
|
|
351
|
+
expect(record.boundaries.lightweightOperation).toHaveLength(callCount)
|
|
352
|
+
|
|
353
|
+
// Verify all boundary calls have timing
|
|
354
|
+
record.boundaries.lightweightOperation.forEach((call, index) => {
|
|
355
|
+
expect(call.timing).toBeDefined()
|
|
356
|
+
expect(call.input).toEqual([index])
|
|
357
|
+
expect(call.output).toEqual({ index, doubled: index * 2 })
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// Performance should be reasonable for high volume
|
|
361
|
+
expect(executionTime).toBeLessThan(5000) // Less than 5 seconds for 1000 calls
|
|
362
|
+
|
|
363
|
+
console.log(`Extreme boundary test: ${callCount} calls in ${executionTime}ms`)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('should handle schema validation edge cases with timing', async () => {
|
|
367
|
+
const validationTask = createTask({
|
|
368
|
+
name: 'validation-edge-case-test',
|
|
369
|
+
schema: new Schema({
|
|
370
|
+
requiredString: Schema.string(),
|
|
371
|
+
optionalNumber: Schema.number().optional(),
|
|
372
|
+
nestedValue: Schema.number()
|
|
373
|
+
}),
|
|
374
|
+
boundaries: {},
|
|
375
|
+
fn: async ({ requiredString, optionalNumber, nestedValue }) => {
|
|
376
|
+
return {
|
|
377
|
+
processed: true,
|
|
378
|
+
stringLength: requiredString.length,
|
|
379
|
+
hasOptional: optionalNumber !== undefined,
|
|
380
|
+
nestedValue: nestedValue
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Test with valid input
|
|
386
|
+
const [validResult, validError, validRecord] = await validationTask.safeRun({
|
|
387
|
+
requiredString: 'test',
|
|
388
|
+
optionalNumber: 42,
|
|
389
|
+
nestedValue: 100
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
expect(validError).toBeNull()
|
|
393
|
+
expect(validResult?.processed).toBe(true)
|
|
394
|
+
expect(validRecord.timing).toBeDefined()
|
|
395
|
+
expect(validRecord.type).toBe('success')
|
|
396
|
+
|
|
397
|
+
// Test with invalid input (should fail validation before timing main function)
|
|
398
|
+
const [invalidResult, invalidError, invalidRecord] = await validationTask.safeRun({
|
|
399
|
+
requiredString: 123, // Wrong type
|
|
400
|
+
nestedValue: 'not-a-number' // Wrong type
|
|
401
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
402
|
+
} as any)
|
|
403
|
+
|
|
404
|
+
expect(invalidResult).toBeNull()
|
|
405
|
+
expect(invalidError).not.toBeNull()
|
|
406
|
+
expect(invalidRecord.type).toBe('error')
|
|
407
|
+
expect(invalidRecord.timing).toBeUndefined() // No main function timing on validation error
|
|
408
|
+
|
|
409
|
+
// Test with missing required field
|
|
410
|
+
const [missingResult, missingError, missingRecord] = await validationTask.safeRun({
|
|
411
|
+
nestedValue: 100
|
|
412
|
+
// Missing requiredString
|
|
413
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
414
|
+
} as any)
|
|
415
|
+
|
|
416
|
+
expect(missingResult).toBeNull()
|
|
417
|
+
expect(missingError).not.toBeNull()
|
|
418
|
+
expect(missingRecord.type).toBe('error')
|
|
419
|
+
expect(missingRecord.timing).toBeUndefined()
|
|
420
|
+
|
|
421
|
+
console.log(`Schema validation edge cases: valid=${validRecord.type}, invalid=${invalidRecord.type}, missing=${missingRecord.type}`)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('should handle very large execution records efficiently', async () => {
|
|
425
|
+
const largeRecordTask = createTask({
|
|
426
|
+
name: 'large-record-test',
|
|
427
|
+
schema: new Schema({
|
|
428
|
+
dataSize: Schema.number(),
|
|
429
|
+
metricCount: Schema.number()
|
|
430
|
+
}),
|
|
431
|
+
boundaries: {
|
|
432
|
+
generateLargeData: async (size: number) => {
|
|
433
|
+
const data = []
|
|
434
|
+
for (let i = 0; i < size; i++) {
|
|
435
|
+
data.push({
|
|
436
|
+
id: `item_${i}`,
|
|
437
|
+
data: new Array(100).fill(Math.random()),
|
|
438
|
+
metadata: { index: i, category: `cat_${i % 10}` }
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
return { items: data, totalSize: size }
|
|
442
|
+
},
|
|
443
|
+
processLargeDataset: async (dataset: { items: Array<{ id: string; data: number[] }> }) => {
|
|
444
|
+
// Simulate heavy processing
|
|
445
|
+
const processed = dataset.items.map((item: { id: string; data: number[] }) => ({
|
|
446
|
+
id: item.id,
|
|
447
|
+
processed: true,
|
|
448
|
+
checksum: item.data.reduce((sum: number, val: number) => sum + val, 0)
|
|
449
|
+
}))
|
|
450
|
+
return { processed, count: processed.length }
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
fn: async ({ dataSize, metricCount }, { generateLargeData, processLargeDataset, setMetrics, setMetadata }) => {
|
|
454
|
+
await setMetadata('dataSize', dataSize.toString())
|
|
455
|
+
await setMetadata('expectedMetrics', metricCount.toString())
|
|
456
|
+
|
|
457
|
+
// Generate large dataset
|
|
458
|
+
const dataset = await generateLargeData(dataSize)
|
|
459
|
+
await setMetrics({ type: 'business', name: 'data_generated', value: dataset.totalSize })
|
|
460
|
+
|
|
461
|
+
// Generate many metrics
|
|
462
|
+
for (let i = 0; i < metricCount; i++) {
|
|
463
|
+
await setMetrics({
|
|
464
|
+
type: i % 3 === 0 ? 'business' : i % 3 === 1 ? 'performance' : 'error',
|
|
465
|
+
name: `large_record_metric_${i}`,
|
|
466
|
+
value: Math.random() * 1000
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Process the large dataset
|
|
471
|
+
const result = await processLargeDataset(dataset)
|
|
472
|
+
await setMetrics({ type: 'business', name: 'items_processed', value: result.count })
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
dataSize: dataset.totalSize,
|
|
476
|
+
processedCount: result.count,
|
|
477
|
+
metricsGenerated: metricCount,
|
|
478
|
+
status: 'completed'
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
const memoryBefore = process.memoryUsage()
|
|
484
|
+
const start = Date.now()
|
|
485
|
+
|
|
486
|
+
const [result, error, record] = await largeRecordTask.safeRun({
|
|
487
|
+
dataSize: 100, // 100 items with 100 random numbers each
|
|
488
|
+
metricCount: 200 // 200 metrics
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
const executionTime = Date.now() - start
|
|
492
|
+
const memoryAfter = process.memoryUsage()
|
|
493
|
+
|
|
494
|
+
expect(error).toBeNull()
|
|
495
|
+
expect(result?.status).toBe('completed')
|
|
496
|
+
expect(result?.dataSize).toBe(100)
|
|
497
|
+
expect(result?.metricsGenerated).toBe(200)
|
|
498
|
+
|
|
499
|
+
// Verify record structure
|
|
500
|
+
expect(record.metrics).toHaveLength(202) // 200 + 2 additional metrics
|
|
501
|
+
expect(record.boundaries.generateLargeData).toHaveLength(1)
|
|
502
|
+
expect(record.boundaries.processLargeDataset).toHaveLength(1)
|
|
503
|
+
|
|
504
|
+
// Verify large data in boundaries is preserved
|
|
505
|
+
const generatedData = record.boundaries.generateLargeData[0].output
|
|
506
|
+
if (generatedData) {
|
|
507
|
+
expect(generatedData.items).toHaveLength(100)
|
|
508
|
+
expect(generatedData.items[0].data).toHaveLength(100)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Performance should be reasonable even with large records
|
|
512
|
+
expect(executionTime).toBeLessThan(2000) // Less than 2 seconds
|
|
513
|
+
|
|
514
|
+
const memoryIncrease = memoryAfter.heapUsed - memoryBefore.heapUsed
|
|
515
|
+
expect(memoryIncrease).toBeLessThan(100 * 1024 * 1024) // Less than 100MB
|
|
516
|
+
|
|
517
|
+
console.log(`Large record test: ${result?.dataSize} items, ${result?.metricsGenerated} metrics, ${executionTime}ms, ${(memoryIncrease / 1024 / 1024).toFixed(1)}MB`)
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
describe('Timing accuracy and precision tests', () => {
|
|
522
|
+
it('should maintain timing accuracy across different execution patterns', async () => {
|
|
523
|
+
const precisionTask = createTask({
|
|
524
|
+
name: 'timing-precision-test',
|
|
525
|
+
schema: new Schema({ pattern: Schema.string() }),
|
|
526
|
+
boundaries: {
|
|
527
|
+
fixedDelay: async (ms: number) => {
|
|
528
|
+
await new Promise(resolve => setTimeout(resolve, ms))
|
|
529
|
+
return { delayed: ms }
|
|
530
|
+
},
|
|
531
|
+
variableDelay: async (baseMs: number) => {
|
|
532
|
+
const actualDelay = baseMs + Math.random() * 20 - 10 // ±10ms variation
|
|
533
|
+
await new Promise(resolve => setTimeout(resolve, actualDelay))
|
|
534
|
+
return { delayed: actualDelay }
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
fn: async ({ pattern }, { fixedDelay, variableDelay, setMetrics }) => {
|
|
538
|
+
const timings = []
|
|
539
|
+
|
|
540
|
+
if (pattern === 'fixed') {
|
|
541
|
+
// Test fixed delays
|
|
542
|
+
const delays = [50, 100, 150]
|
|
543
|
+
for (const delay of delays) {
|
|
544
|
+
const start = Date.now()
|
|
545
|
+
await fixedDelay(delay)
|
|
546
|
+
const measured = Date.now() - start
|
|
547
|
+
timings.push({ expected: delay, measured })
|
|
548
|
+
await setMetrics({ type: 'performance', name: 'timing_accuracy', value: Math.abs(measured - delay) })
|
|
549
|
+
}
|
|
550
|
+
} else if (pattern === 'variable') {
|
|
551
|
+
// Test variable delays
|
|
552
|
+
for (let i = 0; i < 5; i++) {
|
|
553
|
+
const baseDelay = 80
|
|
554
|
+
const start = Date.now()
|
|
555
|
+
const result = await variableDelay(baseDelay)
|
|
556
|
+
const measured = Date.now() - start
|
|
557
|
+
timings.push({ expected: result.delayed, measured })
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return { pattern, timings }
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// Test fixed timing precision
|
|
566
|
+
const [fixedResult, fixedError, fixedRecord] = await precisionTask.safeRun({ pattern: 'fixed' })
|
|
567
|
+
|
|
568
|
+
expect(fixedError).toBeNull()
|
|
569
|
+
expect(fixedResult?.timings).toHaveLength(3)
|
|
570
|
+
|
|
571
|
+
// Verify boundary timing accuracy
|
|
572
|
+
fixedRecord.boundaries.fixedDelay.forEach((call, index) => {
|
|
573
|
+
const expectedDelay = [50, 100, 150][index]
|
|
574
|
+
const actualDuration = call.timing.duration
|
|
575
|
+
const accuracy = Math.abs(actualDuration! - expectedDelay)
|
|
576
|
+
|
|
577
|
+
// Timing should be within 30ms of expected (allowing for system variance)
|
|
578
|
+
expect(accuracy).toBeLessThan(30)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// Test variable timing
|
|
582
|
+
const [variableResult, variableError, variableRecord] = await precisionTask.safeRun({ pattern: 'variable' })
|
|
583
|
+
|
|
584
|
+
expect(variableError).toBeNull()
|
|
585
|
+
expect(variableResult?.timings).toHaveLength(5)
|
|
586
|
+
|
|
587
|
+
// Verify variable timing boundaries
|
|
588
|
+
variableRecord.boundaries.variableDelay.forEach((call) => {
|
|
589
|
+
expect(call.timing.duration).toBeGreaterThan(60) // Should be at least 70ms (80-10)
|
|
590
|
+
expect(call.timing.duration).toBeLessThan(120) // Should be at most 110ms (80+10+20 for variance)
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
console.log(`Timing precision test completed: fixed=${fixedRecord.boundaries.fixedDelay.length} calls, variable=${variableRecord.boundaries.variableDelay.length} calls`)
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
})
|
|
@@ -12,7 +12,15 @@ describe('Run boundary tests', function () {
|
|
|
12
12
|
const runTape = identity.getRunData()
|
|
13
13
|
|
|
14
14
|
expect(runTape.length).toBe(1)
|
|
15
|
-
expect(runTape[0]).toEqual({
|
|
15
|
+
expect(runTape[0]).toEqual({
|
|
16
|
+
input: [{ value: 5 }],
|
|
17
|
+
output: { value: 5 },
|
|
18
|
+
timing: expect.objectContaining({
|
|
19
|
+
startTime: expect.any(Number),
|
|
20
|
+
endTime: expect.any(Number),
|
|
21
|
+
duration: expect.any(Number)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
16
24
|
})
|
|
17
25
|
|
|
18
26
|
it('Should only add a record if run is active', async function () {
|
|
@@ -41,7 +49,15 @@ describe('Run boundary tests', function () {
|
|
|
41
49
|
const runTape = identity.getRunData()
|
|
42
50
|
|
|
43
51
|
expect(runTape.length).toBe(1)
|
|
44
|
-
expect(runTape[0]).toEqual({
|
|
52
|
+
expect(runTape[0]).toEqual({
|
|
53
|
+
input: [{ value: 5 }],
|
|
54
|
+
output: { value: 5 },
|
|
55
|
+
timing: expect.objectContaining({
|
|
56
|
+
startTime: expect.any(Number),
|
|
57
|
+
endTime: expect.any(Number),
|
|
58
|
+
duration: expect.any(Number)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
45
61
|
})
|
|
46
62
|
|
|
47
63
|
it('Should have run elements from this run', async function () {
|
|
@@ -59,6 +75,14 @@ describe('Run boundary tests', function () {
|
|
|
59
75
|
const runTape = identity.getRunData()
|
|
60
76
|
|
|
61
77
|
expect(runTape.length).toBe(1)
|
|
62
|
-
expect(runTape[0]).toEqual({
|
|
78
|
+
expect(runTape[0]).toEqual({
|
|
79
|
+
input: [{ value: 5 }],
|
|
80
|
+
output: { value: 5 },
|
|
81
|
+
timing: expect.objectContaining({
|
|
82
|
+
startTime: expect.any(Number),
|
|
83
|
+
endTime: expect.any(Number),
|
|
84
|
+
duration: expect.any(Number)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
63
87
|
})
|
|
64
88
|
})
|