Skip to content

Commit 73a5d6f

Browse files
committed
expand integration tests for non latest node
1 parent 45273a3 commit 73a5d6f

10 files changed

Lines changed: 903 additions & 6 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"description": "graphql-js tracing channels should publish on node:diagnostics_channel (Bun)",
3+
"private": true,
4+
"scripts": {
5+
"test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app oven/bun:\"$BUN_VERSION\"-slim bun test.js"
6+
},
7+
"dependencies": {
8+
"graphql": "file:../graphql.tgz"
9+
}
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"imports": {
3+
"graphql": "../graphql-deno-dist/index.ts",
4+
"graphql/": "../graphql-deno-dist/"
5+
}
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"description": "graphql-js tracing channels should publish on node:diagnostics_channel (Deno with deno build)",
3+
"private": true,
4+
"scripts": {
5+
"test": "docker run --rm --volume \"$PWD/..\":/usr/src/app -w /usr/src/app/diagnostics-deno-with-deno-build denoland/deno:alpine-\"$DENO_VERSION\" deno run test.js"
6+
}
7+
}
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
// TracingChannel is marked experimental in Node's docs but is shipped on
2+
// every runtime graphql-js supports. This test exercises it directly.
3+
/* eslint-disable n/no-unsupported-features/node-builtins */
4+
5+
import assert from 'node:assert/strict';
6+
import { AsyncLocalStorage } from 'node:async_hooks';
7+
import dc from 'node:diagnostics_channel';
8+
9+
import { buildSchema, execute, parse, subscribe, validate } from 'graphql';
10+
11+
function runParseCases() {
12+
// graphql:parse - synchronous.
13+
{
14+
const events = [];
15+
const handler = {
16+
start: (msg) => events.push({ kind: 'start', source: msg.source }),
17+
end: (msg) => events.push({ kind: 'end', source: msg.source }),
18+
asyncStart: (msg) =>
19+
events.push({ kind: 'asyncStart', source: msg.source }),
20+
asyncEnd: (msg) => events.push({ kind: 'asyncEnd', source: msg.source }),
21+
error: (msg) =>
22+
events.push({ kind: 'error', source: msg.source, error: msg.error }),
23+
};
24+
25+
const channel = dc.tracingChannel('graphql:parse');
26+
channel.subscribe(handler);
27+
28+
try {
29+
const doc = parse('{ field }');
30+
assert.equal(doc.kind, 'Document');
31+
assert.deepEqual(
32+
events.map((e) => e.kind),
33+
['start', 'end'],
34+
);
35+
assert.equal(events[0].source, '{ field }');
36+
assert.equal(events[1].source, '{ field }');
37+
} finally {
38+
channel.unsubscribe(handler);
39+
}
40+
}
41+
42+
// graphql:parse - error path fires start, error, end.
43+
{
44+
const events = [];
45+
const handler = {
46+
start: (msg) => events.push({ kind: 'start', source: msg.source }),
47+
end: (msg) => events.push({ kind: 'end', source: msg.source }),
48+
error: (msg) =>
49+
events.push({ kind: 'error', source: msg.source, error: msg.error }),
50+
};
51+
52+
const channel = dc.tracingChannel('graphql:parse');
53+
channel.subscribe(handler);
54+
55+
try {
56+
assert.throws(() => parse('{ '));
57+
assert.deepEqual(
58+
events.map((e) => e.kind),
59+
['start', 'error', 'end'],
60+
);
61+
assert.ok(events[1].error instanceof Error);
62+
} finally {
63+
channel.unsubscribe(handler);
64+
}
65+
}
66+
}
67+
68+
function runValidateCase() {
69+
const schema = buildSchema(`type Query { field: String }`);
70+
const doc = parse('{ field }');
71+
72+
const events = [];
73+
const handler = {
74+
start: (msg) =>
75+
events.push({
76+
kind: 'start',
77+
schema: msg.schema,
78+
document: msg.document,
79+
}),
80+
end: () => events.push({ kind: 'end' }),
81+
error: (msg) => events.push({ kind: 'error', error: msg.error }),
82+
};
83+
84+
const channel = dc.tracingChannel('graphql:validate');
85+
channel.subscribe(handler);
86+
87+
try {
88+
const errors = validate(schema, doc);
89+
assert.deepEqual(errors, []);
90+
assert.deepEqual(
91+
events.map((e) => e.kind),
92+
['start', 'end'],
93+
);
94+
assert.equal(events[0].schema, schema);
95+
assert.equal(events[0].document, doc);
96+
} finally {
97+
channel.unsubscribe(handler);
98+
}
99+
}
100+
101+
function runExecuteCase() {
102+
const schema = buildSchema(`type Query { hello: String }`);
103+
const document = parse('query Greeting { hello }');
104+
105+
const events = [];
106+
const handler = {
107+
start: (msg) =>
108+
events.push({
109+
kind: 'start',
110+
operationType: msg.operationType,
111+
operationName: msg.operationName,
112+
document: msg.document,
113+
schema: msg.schema,
114+
}),
115+
end: () => events.push({ kind: 'end' }),
116+
asyncStart: () => events.push({ kind: 'asyncStart' }),
117+
asyncEnd: () => events.push({ kind: 'asyncEnd' }),
118+
error: (msg) => events.push({ kind: 'error', error: msg.error }),
119+
};
120+
121+
const channel = dc.tracingChannel('graphql:execute');
122+
channel.subscribe(handler);
123+
124+
try {
125+
const result = execute({
126+
schema,
127+
document,
128+
rootValue: { hello: 'world' },
129+
});
130+
assert.equal(result.data.hello, 'world');
131+
assert.deepEqual(
132+
events.map((e) => e.kind),
133+
['start', 'end'],
134+
);
135+
assert.equal(events[0].operationType, 'query');
136+
assert.equal(events[0].operationName, 'Greeting');
137+
assert.equal(events[0].document, document);
138+
assert.equal(events[0].schema, schema);
139+
} finally {
140+
channel.unsubscribe(handler);
141+
}
142+
}
143+
144+
async function runSubscribeCase() {
145+
async function* ticks() {
146+
yield { tick: 'one' };
147+
}
148+
149+
const schema = buildSchema(`
150+
type Query { dummy: String }
151+
type Subscription { tick: String }
152+
`);
153+
// buildSchema doesn't attach a subscribe resolver to fields; inject one.
154+
schema.getSubscriptionType().getFields().tick.subscribe = () => ticks();
155+
156+
const document = parse('subscription Tick { tick }');
157+
158+
const events = [];
159+
const handler = {
160+
start: (msg) =>
161+
events.push({
162+
kind: 'start',
163+
operationType: msg.operationType,
164+
operationName: msg.operationName,
165+
}),
166+
end: () => events.push({ kind: 'end' }),
167+
asyncStart: () => events.push({ kind: 'asyncStart' }),
168+
asyncEnd: () => events.push({ kind: 'asyncEnd' }),
169+
error: (msg) => events.push({ kind: 'error', error: msg.error }),
170+
};
171+
172+
const channel = dc.tracingChannel('graphql:subscribe');
173+
channel.subscribe(handler);
174+
175+
try {
176+
const result = subscribe({ schema, document });
177+
const stream = typeof result.then === 'function' ? await result : result;
178+
if (stream[Symbol.asyncIterator]) {
179+
await stream.return?.();
180+
}
181+
// Subscription setup is synchronous here; start/end fire, no async tail.
182+
assert.deepEqual(
183+
events.map((e) => e.kind),
184+
['start', 'end'],
185+
);
186+
assert.equal(events[0].operationType, 'subscription');
187+
assert.equal(events[0].operationName, 'Tick');
188+
} finally {
189+
channel.unsubscribe(handler);
190+
}
191+
}
192+
193+
function runResolveCase() {
194+
const schema = buildSchema(
195+
`type Query { hello: String nested: Nested } type Nested { leaf: String }`,
196+
);
197+
const document = parse('{ hello nested { leaf } }');
198+
199+
const events = [];
200+
const handler = {
201+
start: (msg) =>
202+
events.push({
203+
kind: 'start',
204+
fieldName: msg.fieldName,
205+
parentType: msg.parentType,
206+
fieldType: msg.fieldType,
207+
fieldPath: msg.fieldPath,
208+
isDefaultResolver: msg.isDefaultResolver,
209+
}),
210+
end: () => events.push({ kind: 'end' }),
211+
asyncStart: () => events.push({ kind: 'asyncStart' }),
212+
asyncEnd: () => events.push({ kind: 'asyncEnd' }),
213+
error: (msg) => events.push({ kind: 'error', error: msg.error }),
214+
};
215+
216+
const channel = dc.tracingChannel('graphql:resolve');
217+
channel.subscribe(handler);
218+
219+
try {
220+
const rootValue = { hello: () => 'world', nested: { leaf: 'leaf-value' } };
221+
execute({ schema, document, rootValue });
222+
223+
const starts = events.filter((e) => e.kind === 'start');
224+
const paths = starts.map((e) => e.fieldPath);
225+
assert.deepEqual(paths, ['hello', 'nested', 'nested.leaf']);
226+
227+
const hello = starts.find((e) => e.fieldName === 'hello');
228+
assert.equal(hello.parentType, 'Query');
229+
assert.equal(hello.fieldType, 'String');
230+
// buildSchema never attaches field.resolve; all fields report as trivial.
231+
assert.equal(hello.isDefaultResolver, true);
232+
} finally {
233+
channel.unsubscribe(handler);
234+
}
235+
}
236+
237+
function runNoSubscriberCase() {
238+
const doc = parse('{ field }');
239+
assert.equal(doc.kind, 'Document');
240+
}
241+
242+
async function runAlsPropagationCase() {
243+
// A subscriber that binds a store on the `start` sub-channel should be able
244+
// to read it in every lifecycle handler (start, end, asyncStart, asyncEnd).
245+
// This is what APMs use to parent child spans to the current operation
246+
// without threading state through the ctx object.
247+
const als = new AsyncLocalStorage();
248+
const channel = dc.tracingChannel('graphql:execute');
249+
channel.start.bindStore(als, (ctx) => ({ operationName: ctx.operationName }));
250+
251+
const seen = {};
252+
const handler = {
253+
start: () => (seen.start = als.getStore()),
254+
end: () => (seen.end = als.getStore()),
255+
asyncStart: () => (seen.asyncStart = als.getStore()),
256+
asyncEnd: () => (seen.asyncEnd = als.getStore()),
257+
};
258+
channel.subscribe(handler);
259+
260+
try {
261+
const schema = buildSchema(`type Query { slow: String }`);
262+
const document = parse('query Slow { slow }');
263+
const rootValue = { slow: () => Promise.resolve('done') };
264+
265+
await execute({ schema, document, rootValue });
266+
267+
assert.deepEqual(seen.start, { operationName: 'Slow' });
268+
assert.deepEqual(seen.end, { operationName: 'Slow' });
269+
assert.deepEqual(seen.asyncStart, { operationName: 'Slow' });
270+
assert.deepEqual(seen.asyncEnd, { operationName: 'Slow' });
271+
} finally {
272+
channel.unsubscribe(handler);
273+
channel.start.unbindStore(als);
274+
}
275+
}
276+
277+
async function main() {
278+
runParseCases();
279+
runValidateCase();
280+
runExecuteCase();
281+
await runSubscribeCase();
282+
runResolveCase();
283+
await runAlsPropagationCase();
284+
runNoSubscriberCase();
285+
console.log('diagnostics integration test passed');
286+
}
287+
288+
main();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"description": "graphql-js tracing channels should publish on node:diagnostics_channel (Deno with node build)",
3+
"private": true,
4+
"scripts": {
5+
"test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app denoland/deno:alpine-\"$DENO_VERSION\" deno run --conditions=development test.js"
6+
},
7+
"dependencies": {
8+
"graphql": "file:../graphql.tgz"
9+
}
10+
}

0 commit comments

Comments
 (0)