Skip to content

Commit

Permalink
ignore coverage and skip abortion test if node doesn't support AbortC…
Browse files Browse the repository at this point in the history
…ontroller
  • Loading branch information
igrlk committed Feb 11, 2023
1 parent e4b51d3 commit 8f84774
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 69 deletions.
138 changes: 72 additions & 66 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { describe, it } from 'mocha';
import { expectJSON } from '../../__testUtils__/expectJSON.js';
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';

import { AbortController } from '../../jsutils/AbortController.js';
import {
AbortController,
hasAbortControllerSupport,
} from '../../jsutils/AbortController.js';
import { inspect } from '../../jsutils/inspect.js';

import { Kind } from '../../language/kinds.js';
Expand Down Expand Up @@ -1315,59 +1318,60 @@ describe('Execute: Handles basic execution tasks', () => {
expect(possibleTypes).to.deep.equal([fooObject]);
});

it('stops execution and throws an error when signal is aborted', async () => {
/**
* This test has 3 resolvers nested in each other.
* Every resolve function waits 200ms before returning data.
*
* The test waits for the first resolver and half of the 2nd resolver execution time (200ms + 100ms)
* and then aborts the execution.
*
* 2nd resolver execution finishes, and we then expect to not execute the 3rd resolver
* and to get an error about aborted operation.
*/

const WAIT_MS_BEFORE_RESOLVING = 200;
const ABORT_IN_MS_AFTER_STARTING_EXECUTION =
WAIT_MS_BEFORE_RESOLVING + WAIT_MS_BEFORE_RESOLVING / 2;

const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
resolvesIn500ms: {
type: new GraphQLObjectType({
name: 'ResolvesIn500ms',
fields: {
resolvesIn400ms: {
type: new GraphQLObjectType({
name: 'ResolvesIn400ms',
fields: {
shouldNotBeResolved: {
type: GraphQLString,
/* c8 ignore next 3 */
resolve: () => {
throw new Error('This should not be executed!');
/* c8 ignore start */
if (hasAbortControllerSupport) {
it('stops execution and throws an error when signal is aborted', async () => {
/**
* This test has 3 resolvers nested in each other.
* Every resolve function waits 200ms before returning data.
*
* The test waits for the first resolver and half of the 2nd resolver execution time (200ms + 100ms)
* and then aborts the execution.
*
* 2nd resolver execution finishes, and we then expect to not execute the 3rd resolver
* and to get an error about aborted operation.
*/

const WAIT_MS_BEFORE_RESOLVING = 200;
const ABORT_IN_MS_AFTER_STARTING_EXECUTION =
WAIT_MS_BEFORE_RESOLVING + WAIT_MS_BEFORE_RESOLVING / 2;

const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
resolvesIn500ms: {
type: new GraphQLObjectType({
name: 'ResolvesIn500ms',
fields: {
resolvesIn400ms: {
type: new GraphQLObjectType({
name: 'ResolvesIn400ms',
fields: {
shouldNotBeResolved: {
type: GraphQLString,
resolve: () => {
throw new Error('This should not be executed!');
},
},
},
},
}),
resolve: () =>
new Promise((resolve) => {
setTimeout(() => resolve({}), WAIT_MS_BEFORE_RESOLVING);
}),
resolve: () =>
new Promise((resolve) => {
setTimeout(() => resolve({}), WAIT_MS_BEFORE_RESOLVING);
}),
},
},
},
}),
resolve: () =>
new Promise((resolve) => {
setTimeout(() => resolve({}), WAIT_MS_BEFORE_RESOLVING);
}),
resolve: () =>
new Promise((resolve) => {
setTimeout(() => resolve({}), WAIT_MS_BEFORE_RESOLVING);
}),
},
},
},
}),
});
const document = parse(`
}),
});
const document = parse(`
query {
resolvesIn500ms {
resolvesIn400ms {
Expand All @@ -1377,22 +1381,24 @@ describe('Execute: Handles basic execution tasks', () => {
}
`);

const abortController = new AbortController();
const executionPromise = execute({
schema,
document,
signal: abortController.signal,
});

setTimeout(
() => abortController.abort(),
ABORT_IN_MS_AFTER_STARTING_EXECUTION,
);

const result = await executionPromise;
expect(result.errors?.[0].message).to.eq('Execution aborted.');
expect(result.data).to.eql({
resolvesIn500ms: { resolvesIn400ms: null },
});
});
const abortController = new AbortController();
const executionPromise = execute({
schema,
document,
signal: abortController.signal,
});

setTimeout(
() => abortController.abort(),
ABORT_IN_MS_AFTER_STARTING_EXECUTION,
);

const result = await executionPromise;
expect(result.errors?.[0].message).to.eq('Execution aborted.');
expect(result.data).to.eql({
resolvesIn500ms: { resolvesIn400ms: null },
});
});
}
/* c8 ignore stop */
});
11 changes: 8 additions & 3 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export function experimentalExecuteIncrementally(
return executeImpl(exeContext);
}

/* c8 ignore start */
function subscribeToAbortSignal(exeContext: ExecutionContext): () => void {
const { abortion } = exeContext;
if (!abortion) {
Expand All @@ -362,6 +363,7 @@ function subscribeToAbortSignal(exeContext: ExecutionContext): () => void {
abortion.executionAbortController.abort();
};
}
/* c8 ignore stop */

function executeImpl(
exeContext: ExecutionContext,
Expand Down Expand Up @@ -546,6 +548,7 @@ export function buildExecutionContext(
};
}

/* c8 ignore start */
function getContextAbortionEntities(
passedInAbortSignal: Maybe<IAbortSignal>,
): ExecutionContext['abortion'] {
Expand All @@ -561,6 +564,7 @@ function getContextAbortionEntities(
executionAbortSignal: executionAbortController.signal,
};
}
/* c8 ignore stop */

function buildPerEventExecutionContext(
exeContext: ExecutionContext,
Expand Down Expand Up @@ -892,12 +896,11 @@ function completeValue(
result: unknown,
asyncPayloadRecord?: AsyncPayloadRecord,
): PromiseOrValue<unknown> {
// Ignoring test coverage for abortion check since Node 14 doesn't support AbortSignal
// and this condition is never true.
/* c8 ignore next 3 */
/* c8 ignore start */
if (exeContext.abortion?.executionAbortSignal.aborted) {
throw new GraphQLError('Execution aborted.');
}
/* c8 ignore stop */

// If result is an Error, throw a located error.
if (result instanceof Error) {
Expand Down Expand Up @@ -1636,11 +1639,13 @@ export function subscribe(
> {
// Until we have execution cancelling support in Subscriptions,
// throw an error if client provides abort signal.
/* c8 ignore start */
if (args.signal) {
return {
errors: [new GraphQLError('Subscriptions do not support abort signals.')],
};
}
/* c8 ignore stop */

// If a valid execution context cannot be created due to incorrect arguments,
// a "Response" with only errors is returned.
Expand Down
5 changes: 5 additions & 0 deletions src/jsutils/AbortController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ export const AbortController: new () => IAbortController =
this._signal.aborted = true;
}
};

export const hasAbortControllerSupport =
// eslint-disable-next-line no-undef
Boolean(global.AbortController);

/* c8 ignore stop */

0 comments on commit 8f84774

Please sign in to comment.