autotel-vitest 0.4.33 → 0.4.35
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/package.json +2 -3
- package/src/fixture.ts +0 -70
- package/src/index.test.ts +0 -197
- package/src/index.ts +0 -52
- package/src/reporter.test.ts +0 -193
- package/src/reporter.ts +0 -119
- package/src/wiring.test.ts +0 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel-vitest",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.35",
|
|
4
4
|
"description": "Vitest fixture for OpenTelemetry: one span per test so all instrumented code is filterable by test run",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,12 +21,11 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"dist",
|
|
24
|
-
"src",
|
|
25
24
|
"README.md",
|
|
26
25
|
"skills"
|
|
27
26
|
],
|
|
28
27
|
"dependencies": {
|
|
29
|
-
"autotel": "4.1
|
|
28
|
+
"autotel": "4.2.1"
|
|
30
29
|
},
|
|
31
30
|
"peerDependencies": {
|
|
32
31
|
"vitest": ">=4.1.8"
|
package/src/fixture.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getTracer,
|
|
3
|
-
getAutotelTracerProvider,
|
|
4
|
-
context as otelContext,
|
|
5
|
-
otelTrace,
|
|
6
|
-
SpanStatusCode,
|
|
7
|
-
} from 'autotel';
|
|
8
|
-
import { TestSpanCollector } from 'autotel/test-span-collector';
|
|
9
|
-
import { SimpleSpanProcessor } from 'autotel/processors';
|
|
10
|
-
|
|
11
|
-
const TRACER_NAME = 'vitest-tests';
|
|
12
|
-
const TRACER_VERSION = '0.1.0';
|
|
13
|
-
|
|
14
|
-
let collector: TestSpanCollector | null = null;
|
|
15
|
-
|
|
16
|
-
interface TracerProviderWithProcessor {
|
|
17
|
-
addSpanProcessor(processor: unknown): void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function ensureCollector(): TestSpanCollector {
|
|
21
|
-
if (!collector) {
|
|
22
|
-
collector = new TestSpanCollector();
|
|
23
|
-
const provider = getAutotelTracerProvider();
|
|
24
|
-
if ('addSpanProcessor' in provider) {
|
|
25
|
-
(provider as TracerProviderWithProcessor).addSpanProcessor(
|
|
26
|
-
new SimpleSpanProcessor(collector),
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return collector;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type OtelFixtureFn = (
|
|
34
|
-
args: { task: { name: string; file?: { name: string }; suite?: { name: string }; meta: Record<string, unknown> } },
|
|
35
|
-
use: (span: unknown) => Promise<void>,
|
|
36
|
-
) => Promise<void>;
|
|
37
|
-
|
|
38
|
-
export const otelTestSpanFixture: [OtelFixtureFn, { auto: true }] = [
|
|
39
|
-
async (
|
|
40
|
-
{ task }: { task: { name: string; file?: { name: string }; suite?: { name: string }; meta: Record<string, unknown> } },
|
|
41
|
-
use: (span: unknown) => Promise<void>,
|
|
42
|
-
) => {
|
|
43
|
-
ensureCollector();
|
|
44
|
-
const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
|
|
45
|
-
const span = tracer.startSpan(`test:${task.name}`, {
|
|
46
|
-
attributes: {
|
|
47
|
-
'test.name': task.name,
|
|
48
|
-
'test.file': task.file?.name ?? '',
|
|
49
|
-
'test.suite': task.suite?.name ?? '',
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
const ctx = otelTrace.setSpan(otelContext.active(), span);
|
|
53
|
-
try {
|
|
54
|
-
await otelContext.with(ctx, () => use(span));
|
|
55
|
-
} catch (error) {
|
|
56
|
-
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
57
|
-
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
58
|
-
throw error;
|
|
59
|
-
} finally {
|
|
60
|
-
span.end();
|
|
61
|
-
const traceId = span.spanContext().traceId;
|
|
62
|
-
const rootSpanId = span.spanContext().spanId;
|
|
63
|
-
const spans = collector!.drainTrace(traceId, rootSpanId);
|
|
64
|
-
if (spans.length > 0) {
|
|
65
|
-
(task.meta as Record<string, unknown>).otelSpans = spans;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
{ auto: true },
|
|
70
|
-
];
|
package/src/index.test.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
let spanIdCounter = 0;
|
|
4
|
-
const createdSpans: Array<{
|
|
5
|
-
end: ReturnType<typeof vi.fn>;
|
|
6
|
-
recordException: ReturnType<typeof vi.fn>;
|
|
7
|
-
setStatus: ReturnType<typeof vi.fn>;
|
|
8
|
-
spanContext: () => { traceId: string; spanId: string };
|
|
9
|
-
}> = [];
|
|
10
|
-
|
|
11
|
-
vi.mock('autotel', () => ({
|
|
12
|
-
SpanStatusCode: { UNSET: 0, OK: 1, ERROR: 2 },
|
|
13
|
-
context: {
|
|
14
|
-
active: () => ({}),
|
|
15
|
-
with: (_ctx: unknown, fn: () => Promise<unknown>) => fn(),
|
|
16
|
-
},
|
|
17
|
-
getTracer: () => ({
|
|
18
|
-
startSpan: (_name: string, _options?: unknown) => {
|
|
19
|
-
const id = String(++spanIdCounter);
|
|
20
|
-
const span = {
|
|
21
|
-
end: vi.fn(),
|
|
22
|
-
recordException: vi.fn(),
|
|
23
|
-
setStatus: vi.fn(),
|
|
24
|
-
spanContext: () => ({ traceId: `trace-${id}`, spanId: `span-${id}` }),
|
|
25
|
-
};
|
|
26
|
-
createdSpans.push(span);
|
|
27
|
-
return span;
|
|
28
|
-
},
|
|
29
|
-
}),
|
|
30
|
-
otelTrace: {
|
|
31
|
-
setSpan: () => ({}),
|
|
32
|
-
},
|
|
33
|
-
getAutotelTracerProvider: vi.fn(() => ({})),
|
|
34
|
-
getTraceContext: vi.fn(() => null),
|
|
35
|
-
resolveTraceUrl: vi.fn(() => undefined),
|
|
36
|
-
isTracing: vi.fn(() => false),
|
|
37
|
-
enrichWithTraceContext: vi.fn((obj: unknown) => obj),
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
let mockDrainResult: unknown[] = [];
|
|
41
|
-
vi.mock('autotel/test-span-collector', () => ({
|
|
42
|
-
TestSpanCollector: class {
|
|
43
|
-
export = vi.fn();
|
|
44
|
-
drainTrace = vi.fn(() => mockDrainResult);
|
|
45
|
-
shutdown = vi.fn(() => Promise.resolve());
|
|
46
|
-
forceFlush = vi.fn(() => Promise.resolve());
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
vi.mock('autotel/processors', () => ({
|
|
51
|
-
SimpleSpanProcessor: class {
|
|
52
|
-
constructor() {}
|
|
53
|
-
},
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
describe('autotel-vitest fixture', () => {
|
|
57
|
-
afterEach(() => {
|
|
58
|
-
createdSpans.length = 0;
|
|
59
|
-
spanIdCounter = 0;
|
|
60
|
-
mockDrainResult = [];
|
|
61
|
-
vi.resetModules();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
async function getFixture() {
|
|
65
|
-
const { otelTestSpanFixture } = await import('./fixture');
|
|
66
|
-
const [fixtureFn, options] = otelTestSpanFixture;
|
|
67
|
-
return { fixtureFn, options, fixture: otelTestSpanFixture };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
it('creates a span for each test via the _otelTestSpan fixture', async () => {
|
|
71
|
-
const { fixtureFn } = await getFixture();
|
|
72
|
-
expect(fixtureFn).toBeTypeOf('function');
|
|
73
|
-
|
|
74
|
-
await fixtureFn(
|
|
75
|
-
{
|
|
76
|
-
task: {
|
|
77
|
-
name: 'creates user',
|
|
78
|
-
file: { name: 'user.test.ts' },
|
|
79
|
-
suite: { name: 'UserService' },
|
|
80
|
-
meta: {},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
async () => {},
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
expect(createdSpans).toHaveLength(1);
|
|
87
|
-
expect(createdSpans[0].end).toHaveBeenCalledTimes(1);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('ends the span after the test completes', async () => {
|
|
91
|
-
const { fixtureFn } = await getFixture();
|
|
92
|
-
|
|
93
|
-
let spanDuringTest: unknown;
|
|
94
|
-
|
|
95
|
-
await fixtureFn(
|
|
96
|
-
{
|
|
97
|
-
task: {
|
|
98
|
-
name: 'test end timing',
|
|
99
|
-
file: { name: 'timing.test.ts' },
|
|
100
|
-
suite: { name: '' },
|
|
101
|
-
meta: {},
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
async (span) => {
|
|
105
|
-
spanDuringTest = span;
|
|
106
|
-
// Span should not yet be ended during the test
|
|
107
|
-
expect(createdSpans[0].end).not.toHaveBeenCalled();
|
|
108
|
-
},
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
// Span should be ended after use() resolves
|
|
112
|
-
expect(spanDuringTest).toBeDefined();
|
|
113
|
-
expect(createdSpans[0].end).toHaveBeenCalledTimes(1);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('sets error status when the test throws', async () => {
|
|
117
|
-
const { fixtureFn } = await getFixture();
|
|
118
|
-
|
|
119
|
-
const err = new Error('test failure');
|
|
120
|
-
|
|
121
|
-
await expect(
|
|
122
|
-
fixtureFn(
|
|
123
|
-
{
|
|
124
|
-
task: {
|
|
125
|
-
name: 'failing test',
|
|
126
|
-
file: { name: 'fail.test.ts' },
|
|
127
|
-
suite: { name: '' },
|
|
128
|
-
meta: {},
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
async () => {
|
|
132
|
-
throw err;
|
|
133
|
-
},
|
|
134
|
-
),
|
|
135
|
-
).rejects.toThrow('test failure');
|
|
136
|
-
|
|
137
|
-
const span = createdSpans[0];
|
|
138
|
-
expect(span.setStatus).toHaveBeenCalledWith({ code: 2 });
|
|
139
|
-
expect(span.recordException).toHaveBeenCalledWith(err);
|
|
140
|
-
expect(span.end).toHaveBeenCalledTimes(1);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('attaches otelSpans to task.meta when collector returns spans', async () => {
|
|
144
|
-
mockDrainResult = [
|
|
145
|
-
{ spanId: 'span-1', name: 'test:my-test', startTimeMs: 1000, durationMs: 100, status: 'ok' },
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
const { fixtureFn } = await getFixture();
|
|
149
|
-
|
|
150
|
-
const meta: Record<string, unknown> = {};
|
|
151
|
-
await fixtureFn(
|
|
152
|
-
{
|
|
153
|
-
task: {
|
|
154
|
-
name: 'my-test',
|
|
155
|
-
file: { name: 'test.ts' },
|
|
156
|
-
suite: { name: '' },
|
|
157
|
-
meta,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
async () => {},
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
expect(meta.otelSpans).toEqual([
|
|
164
|
-
{ spanId: 'span-1', name: 'test:my-test', startTimeMs: 1000, durationMs: 100, status: 'ok' },
|
|
165
|
-
]);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('uses auto: true to activate for every test', async () => {
|
|
169
|
-
const { fixture } = await getFixture();
|
|
170
|
-
expect(Array.isArray(fixture)).toBe(true);
|
|
171
|
-
if (Array.isArray(fixture)) {
|
|
172
|
-
expect(fixture[1]).toEqual({ auto: true });
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe('trace context helper re-exports', () => {
|
|
178
|
-
it('re-exports getTraceContext', async () => {
|
|
179
|
-
const mod = await import('./index');
|
|
180
|
-
expect(mod.getTraceContext).toBeTypeOf('function');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('re-exports resolveTraceUrl', async () => {
|
|
184
|
-
const mod = await import('./index');
|
|
185
|
-
expect(mod.resolveTraceUrl).toBeTypeOf('function');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('re-exports isTracing', async () => {
|
|
189
|
-
const mod = await import('./index');
|
|
190
|
-
expect(mod.isTracing).toBeTypeOf('function');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('re-exports enrichWithTraceContext', async () => {
|
|
194
|
-
const mod = await import('./index');
|
|
195
|
-
expect(mod.enrichWithTraceContext).toBeTypeOf('function');
|
|
196
|
-
});
|
|
197
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* autotel-vitest
|
|
3
|
-
*
|
|
4
|
-
* Vitest fixture that creates one OTel span per test so all autotel-instrumented
|
|
5
|
-
* code executed during a test automatically creates child spans under it;
|
|
6
|
-
* making every test run filterable in your OTLP backend.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* // vitest.config.ts: globalSetup calls init({ service: 'unit-tests' })
|
|
10
|
-
* // In spec:
|
|
11
|
-
* import { test, expect } from 'autotel-vitest';
|
|
12
|
-
* test('creates user', async () => {
|
|
13
|
-
* await userService.createUser({ email: 'test@example.com' });
|
|
14
|
-
* // All trace()/span() calls become children of the test span
|
|
15
|
-
* });
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { test as base } from 'vitest';
|
|
19
|
-
import { otelTestSpanFixture } from './fixture';
|
|
20
|
-
|
|
21
|
-
export const test = base.extend({
|
|
22
|
-
_otelTestSpan: otelTestSpanFixture as never,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export { expect, describe, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
|
|
26
|
-
|
|
27
|
-
// Re-export all autotel/testing utilities
|
|
28
|
-
export {
|
|
29
|
-
createTraceCollector,
|
|
30
|
-
assertTraceCreated,
|
|
31
|
-
assertTraceSucceeded,
|
|
32
|
-
assertTraceFailed,
|
|
33
|
-
assertNoErrors,
|
|
34
|
-
assertTraceDuration,
|
|
35
|
-
waitForTrace,
|
|
36
|
-
getTraceDuration,
|
|
37
|
-
createMockLogger,
|
|
38
|
-
type TraceCollector,
|
|
39
|
-
type TestSpan,
|
|
40
|
-
type LogCollector,
|
|
41
|
-
type LogEntry,
|
|
42
|
-
} from 'autotel/testing';
|
|
43
|
-
|
|
44
|
-
// Re-export trace context helpers for DX convenience
|
|
45
|
-
export {
|
|
46
|
-
getTraceContext,
|
|
47
|
-
resolveTraceUrl,
|
|
48
|
-
isTracing,
|
|
49
|
-
enrichWithTraceContext,
|
|
50
|
-
} from 'autotel';
|
|
51
|
-
|
|
52
|
-
export type { OtelTraceContext } from 'autotel';
|
package/src/reporter.test.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, it, expect, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const spans: Array<{
|
|
4
|
-
end: ReturnType<typeof vi.fn>;
|
|
5
|
-
recordException: ReturnType<typeof vi.fn>;
|
|
6
|
-
setStatus: ReturnType<typeof vi.fn>;
|
|
7
|
-
}> = [];
|
|
8
|
-
|
|
9
|
-
vi.mock('autotel', () => ({
|
|
10
|
-
SpanStatusCode: { ERROR: 2 },
|
|
11
|
-
context: {
|
|
12
|
-
active: () => ({}),
|
|
13
|
-
with: (_ctx: unknown, fn: () => void) => fn(),
|
|
14
|
-
},
|
|
15
|
-
getTracer: () => ({
|
|
16
|
-
startSpan: (_name: string, _options?: unknown) => {
|
|
17
|
-
const span = {
|
|
18
|
-
end: vi.fn(),
|
|
19
|
-
recordException: vi.fn(),
|
|
20
|
-
setStatus: vi.fn(),
|
|
21
|
-
};
|
|
22
|
-
spans.push(span);
|
|
23
|
-
return span;
|
|
24
|
-
},
|
|
25
|
-
}),
|
|
26
|
-
otelTrace: {
|
|
27
|
-
setSpan: () => ({}),
|
|
28
|
-
},
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
function makeTestCase(overrides: {
|
|
32
|
-
id: string;
|
|
33
|
-
name: string;
|
|
34
|
-
moduleId?: string;
|
|
35
|
-
result?: { state: string; errors?: Array<{ message?: string; stack?: string }> };
|
|
36
|
-
}) {
|
|
37
|
-
return {
|
|
38
|
-
id: overrides.id,
|
|
39
|
-
name: overrides.name,
|
|
40
|
-
fullName: overrides.name,
|
|
41
|
-
module: { moduleId: overrides.moduleId ?? 'test.ts' },
|
|
42
|
-
result: () => overrides.result ?? { state: 'passed', errors: undefined },
|
|
43
|
-
} as any;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function makeTestSuite(overrides: {
|
|
47
|
-
id: string;
|
|
48
|
-
name: string;
|
|
49
|
-
moduleId?: string;
|
|
50
|
-
state?: string;
|
|
51
|
-
}) {
|
|
52
|
-
return {
|
|
53
|
-
id: overrides.id,
|
|
54
|
-
name: overrides.name,
|
|
55
|
-
module: { moduleId: overrides.moduleId ?? 'test.ts' },
|
|
56
|
-
state: () => overrides.state ?? 'passed',
|
|
57
|
-
} as any;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
describe('OtelReporter', () => {
|
|
61
|
-
afterEach(() => {
|
|
62
|
-
spans.length = 0;
|
|
63
|
-
vi.resetModules();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('creates a span on test ready and ends it on test result', async () => {
|
|
67
|
-
const { OtelReporter } = await import('./reporter');
|
|
68
|
-
const reporter = new OtelReporter();
|
|
69
|
-
|
|
70
|
-
const testCase = makeTestCase({ id: 'test-1', name: 'my test' });
|
|
71
|
-
|
|
72
|
-
reporter.onTestCaseReady!(testCase);
|
|
73
|
-
expect(spans).toHaveLength(1);
|
|
74
|
-
expect(spans[0].end).not.toHaveBeenCalled();
|
|
75
|
-
|
|
76
|
-
reporter.onTestCaseResult!(testCase);
|
|
77
|
-
expect(spans[0].end).toHaveBeenCalledTimes(1);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('sets error status and records exception on failure', async () => {
|
|
81
|
-
const { OtelReporter } = await import('./reporter');
|
|
82
|
-
const reporter = new OtelReporter();
|
|
83
|
-
|
|
84
|
-
const testCase = makeTestCase({
|
|
85
|
-
id: 'test-fail',
|
|
86
|
-
name: 'failing test',
|
|
87
|
-
result: {
|
|
88
|
-
state: 'failed',
|
|
89
|
-
errors: [{ message: 'assertion failed', stack: 'Error: assertion failed\n at ...' }],
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
reporter.onTestCaseReady!(testCase);
|
|
94
|
-
reporter.onTestCaseResult!(testCase);
|
|
95
|
-
|
|
96
|
-
const span = spans[0];
|
|
97
|
-
expect(span.setStatus).toHaveBeenCalledWith({ code: 2 });
|
|
98
|
-
expect(span.recordException).toHaveBeenCalled();
|
|
99
|
-
expect(span.end).toHaveBeenCalledTimes(1);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('tracks parallel tests independently (no key collisions)', async () => {
|
|
103
|
-
const { OtelReporter } = await import('./reporter');
|
|
104
|
-
const reporter = new OtelReporter();
|
|
105
|
-
|
|
106
|
-
const testA = makeTestCase({ id: 'test-a', name: 'shared title' });
|
|
107
|
-
const testB = makeTestCase({ id: 'test-b', name: 'shared title' });
|
|
108
|
-
|
|
109
|
-
reporter.onTestCaseReady!(testA);
|
|
110
|
-
reporter.onTestCaseReady!(testB);
|
|
111
|
-
|
|
112
|
-
reporter.onTestCaseResult!(testA);
|
|
113
|
-
|
|
114
|
-
expect(spans).toHaveLength(2);
|
|
115
|
-
expect(spans[0].end).toHaveBeenCalledTimes(1);
|
|
116
|
-
expect(spans[1].end).not.toHaveBeenCalled();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('creates suite spans', async () => {
|
|
120
|
-
const { OtelReporter } = await import('./reporter');
|
|
121
|
-
const reporter = new OtelReporter();
|
|
122
|
-
|
|
123
|
-
const suite = makeTestSuite({ id: 'suite-1', name: 'UserService' });
|
|
124
|
-
|
|
125
|
-
reporter.onTestSuiteReady!(suite);
|
|
126
|
-
expect(spans).toHaveLength(1);
|
|
127
|
-
|
|
128
|
-
reporter.onTestSuiteResult!(suite);
|
|
129
|
-
expect(spans[0].end).toHaveBeenCalledTimes(1);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('marks suite span as error when suite fails', async () => {
|
|
133
|
-
const { OtelReporter } = await import('./reporter');
|
|
134
|
-
const reporter = new OtelReporter();
|
|
135
|
-
|
|
136
|
-
const suite = makeTestSuite({ id: 'suite-fail', name: 'FailingSuite', state: 'failed' });
|
|
137
|
-
|
|
138
|
-
reporter.onTestSuiteReady!(suite);
|
|
139
|
-
reporter.onTestSuiteResult!(suite);
|
|
140
|
-
|
|
141
|
-
expect(spans[0].setStatus).toHaveBeenCalledWith({ code: 2 });
|
|
142
|
-
expect(spans[0].end).toHaveBeenCalledTimes(1);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('does not error when onTestCaseResult is called without onTestCaseReady', async () => {
|
|
146
|
-
const { OtelReporter } = await import('./reporter');
|
|
147
|
-
const reporter = new OtelReporter();
|
|
148
|
-
|
|
149
|
-
const testCase = makeTestCase({ id: 'orphan', name: 'orphan test' });
|
|
150
|
-
|
|
151
|
-
// Should not throw
|
|
152
|
-
reporter.onTestCaseResult!(testCase);
|
|
153
|
-
expect(spans).toHaveLength(0);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('does not end test spans from other modules when a module ends', async () => {
|
|
157
|
-
const { OtelReporter } = await import('./reporter');
|
|
158
|
-
const reporter = new OtelReporter();
|
|
159
|
-
|
|
160
|
-
const moduleA = { moduleId: 'a.test.ts' } as any;
|
|
161
|
-
|
|
162
|
-
const testInA = makeTestCase({ id: 'a-1', name: 'test a', moduleId: 'a.test.ts' });
|
|
163
|
-
const testInB = makeTestCase({ id: 'b-1', name: 'test b', moduleId: 'b.test.ts' });
|
|
164
|
-
|
|
165
|
-
reporter.onTestCaseReady!(testInA);
|
|
166
|
-
reporter.onTestCaseReady!(testInB);
|
|
167
|
-
|
|
168
|
-
reporter.onTestModuleEnd!(moduleA);
|
|
169
|
-
|
|
170
|
-
expect(spans).toHaveLength(2);
|
|
171
|
-
expect(spans[0].end).toHaveBeenCalledTimes(1);
|
|
172
|
-
expect(spans[1].end).not.toHaveBeenCalled();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('does not end suite spans from other modules when a module ends', async () => {
|
|
176
|
-
const { OtelReporter } = await import('./reporter');
|
|
177
|
-
const reporter = new OtelReporter();
|
|
178
|
-
|
|
179
|
-
const moduleA = { moduleId: 'a.test.ts' } as any;
|
|
180
|
-
|
|
181
|
-
const suiteInA = makeTestSuite({ id: 'suite-a', name: 'suite a', moduleId: 'a.test.ts' });
|
|
182
|
-
const suiteInB = makeTestSuite({ id: 'suite-b', name: 'suite b', moduleId: 'b.test.ts' });
|
|
183
|
-
|
|
184
|
-
reporter.onTestSuiteReady!(suiteInA);
|
|
185
|
-
reporter.onTestSuiteReady!(suiteInB);
|
|
186
|
-
|
|
187
|
-
reporter.onTestModuleEnd!(moduleA);
|
|
188
|
-
|
|
189
|
-
expect(spans).toHaveLength(2);
|
|
190
|
-
expect(spans[0].end).toHaveBeenCalledTimes(1);
|
|
191
|
-
expect(spans[1].end).not.toHaveBeenCalled();
|
|
192
|
-
});
|
|
193
|
-
});
|
package/src/reporter.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional Vitest reporter that creates OTel spans for each test and suite.
|
|
3
|
-
* Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.
|
|
4
|
-
*
|
|
5
|
-
* Use when you want test/suite timing and hierarchy in OTLP from the runner side.
|
|
6
|
-
* For "test → instrumented code" in one trace (worker side), use the test fixture.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* // vitest.config.ts
|
|
10
|
-
* import { defineConfig } from 'vitest/config';
|
|
11
|
-
*
|
|
12
|
-
* export default defineConfig({
|
|
13
|
-
* test: {
|
|
14
|
-
* reporters: ['default', 'autotel-vitest/reporter'],
|
|
15
|
-
* globalSetup: './globalSetup.ts', // must call init()
|
|
16
|
-
* },
|
|
17
|
-
* });
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { Reporter, TestCase, TestModule, TestSuite } from 'vitest/node';
|
|
21
|
-
import { getTracer, SpanStatusCode } from 'autotel';
|
|
22
|
-
|
|
23
|
-
const TRACER_NAME = 'vitest-reporter';
|
|
24
|
-
const TRACER_VERSION = '0.1.0';
|
|
25
|
-
|
|
26
|
-
type SpanEntry = {
|
|
27
|
-
span: ReturnType<ReturnType<typeof getTracer>['startSpan']>;
|
|
28
|
-
moduleId: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** Convert a vitest TestError-like object to a standard Error for OTel. */
|
|
32
|
-
function toError(testError: { message?: string; stack?: string }): Error {
|
|
33
|
-
const err = new Error(testError.message ?? 'Unknown error');
|
|
34
|
-
if (testError.stack) err.stack = testError.stack;
|
|
35
|
-
return err;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Vitest Reporter that creates one span per test and one per suite (as parents).
|
|
40
|
-
* Requires autotel.init() in globalSetup so spans are exported.
|
|
41
|
-
*/
|
|
42
|
-
class OtelReporter implements Reporter {
|
|
43
|
-
private testSpans = new Map<string, SpanEntry>();
|
|
44
|
-
private suiteSpans = new Map<string, SpanEntry>();
|
|
45
|
-
|
|
46
|
-
onTestCaseReady(testCase: TestCase): void {
|
|
47
|
-
const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
|
|
48
|
-
const moduleId = testCase.module.moduleId ?? '';
|
|
49
|
-
const span = tracer.startSpan(`test:${testCase.name}`, {
|
|
50
|
-
attributes: {
|
|
51
|
-
'test.name': testCase.name,
|
|
52
|
-
'test.fullName': testCase.fullName,
|
|
53
|
-
'test.file': moduleId,
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
this.testSpans.set(testCase.id, { span, moduleId });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
onTestCaseResult(testCase: TestCase): void {
|
|
60
|
-
const entry = this.testSpans.get(testCase.id);
|
|
61
|
-
if (!entry) return;
|
|
62
|
-
|
|
63
|
-
const result = testCase.result();
|
|
64
|
-
if (result.state === 'failed') {
|
|
65
|
-
entry.span.setStatus({ code: SpanStatusCode.ERROR });
|
|
66
|
-
if (result.errors && result.errors.length > 0) {
|
|
67
|
-
for (const error of result.errors) {
|
|
68
|
-
entry.span.recordException(toError(error));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
entry.span.end();
|
|
73
|
-
this.testSpans.delete(testCase.id);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
onTestSuiteReady(testSuite: TestSuite): void {
|
|
77
|
-
const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
|
|
78
|
-
const moduleId = testSuite.module.moduleId ?? '';
|
|
79
|
-
const span = tracer.startSpan(`suite:${testSuite.name}`, {
|
|
80
|
-
attributes: {
|
|
81
|
-
'suite.name': testSuite.name,
|
|
82
|
-
'suite.file': moduleId,
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
this.suiteSpans.set(testSuite.id, { span, moduleId });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
onTestSuiteResult(testSuite: TestSuite): void {
|
|
89
|
-
const entry = this.suiteSpans.get(testSuite.id);
|
|
90
|
-
if (!entry) return;
|
|
91
|
-
|
|
92
|
-
const state = testSuite.state();
|
|
93
|
-
if (state === 'failed') {
|
|
94
|
-
entry.span.setStatus({ code: SpanStatusCode.ERROR });
|
|
95
|
-
}
|
|
96
|
-
entry.span.end();
|
|
97
|
-
this.suiteSpans.delete(testSuite.id);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
onTestModuleEnd(testModule: TestModule): void {
|
|
101
|
-
const moduleId = testModule.moduleId;
|
|
102
|
-
// Clean up any remaining spans for this specific module
|
|
103
|
-
for (const [key, entry] of this.testSpans) {
|
|
104
|
-
if (entry.moduleId === moduleId) {
|
|
105
|
-
entry.span.end();
|
|
106
|
-
this.testSpans.delete(key);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
for (const [key, entry] of this.suiteSpans) {
|
|
110
|
-
if (entry.moduleId === moduleId) {
|
|
111
|
-
entry.span.end();
|
|
112
|
-
this.suiteSpans.delete(key);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export { OtelReporter };
|
|
119
|
-
export default OtelReporter;
|
package/src/wiring.test.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wiring test: verifies the public `test` export from index.ts
|
|
3
|
-
* actually registers the _otelTestSpan fixture with auto: true.
|
|
4
|
-
*
|
|
5
|
-
* Uses the exported `test` (not vitest's base) to define tests,
|
|
6
|
-
* so if index.ts stops calling base.extend() or drops the fixture,
|
|
7
|
-
* these tests will fail.
|
|
8
|
-
*/
|
|
9
|
-
import { expect, vi } from 'vitest';
|
|
10
|
-
|
|
11
|
-
vi.mock('autotel', () => ({
|
|
12
|
-
SpanStatusCode: { UNSET: 0, OK: 1, ERROR: 2 },
|
|
13
|
-
context: {
|
|
14
|
-
active: () => ({}),
|
|
15
|
-
with: (_ctx: unknown, fn: () => Promise<unknown>) => fn(),
|
|
16
|
-
},
|
|
17
|
-
getTracer: () => ({
|
|
18
|
-
startSpan: () => ({
|
|
19
|
-
end: vi.fn(),
|
|
20
|
-
recordException: vi.fn(),
|
|
21
|
-
setStatus: vi.fn(),
|
|
22
|
-
spanContext: () => ({ traceId: 'wiring-trace', spanId: 'wiring-span' }),
|
|
23
|
-
}),
|
|
24
|
-
}),
|
|
25
|
-
otelTrace: {
|
|
26
|
-
setSpan: () => ({}),
|
|
27
|
-
},
|
|
28
|
-
getAutotelTracerProvider: vi.fn(() => ({})),
|
|
29
|
-
getTraceContext: vi.fn(() => null),
|
|
30
|
-
resolveTraceUrl: vi.fn(() => undefined),
|
|
31
|
-
isTracing: vi.fn(() => false),
|
|
32
|
-
enrichWithTraceContext: vi.fn((obj: unknown) => obj),
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
vi.mock('autotel/test-span-collector', () => ({
|
|
36
|
-
TestSpanCollector: class {
|
|
37
|
-
export = vi.fn();
|
|
38
|
-
drainTrace = vi.fn(() => []);
|
|
39
|
-
shutdown = vi.fn(() => Promise.resolve());
|
|
40
|
-
forceFlush = vi.fn(() => Promise.resolve());
|
|
41
|
-
},
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
vi.mock('autotel/processors', () => ({
|
|
45
|
-
SimpleSpanProcessor: class {
|
|
46
|
-
constructor() {}
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
// Import the PUBLIC test — not vitest's base test.
|
|
51
|
-
// If index.ts doesn't wire otelTestSpanFixture via base.extend(),
|
|
52
|
-
// _otelTestSpan won't be available and these tests fail.
|
|
53
|
-
import { test } from './index';
|
|
54
|
-
|
|
55
|
-
test('_otelTestSpan fixture is registered and auto-activates', ({ _otelTestSpan }) => {
|
|
56
|
-
// auto: true means the fixture injects automatically.
|
|
57
|
-
// If the fixture key is missing from base.extend(), this will be undefined.
|
|
58
|
-
expect(_otelTestSpan).toBeDefined();
|
|
59
|
-
});
|