Skip to content

Commit 349dd95

Browse files
committed
feat(core): Emit prepared tests grouped by the contracts being verified - this makes it substantially easier for language DSLs to provide helpful debugging information
1 parent cf2ed8e commit 349dd95

17 files changed

Lines changed: 486 additions & 238 deletions

File tree

packages/case-core/src/connectors/contract/ContractVerifierConnector/ContractVerifierConnector.ts

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,36 @@ import { ReadingCaseContract } from '../../../core/ReadingCaseContract';
1818

1919
import { readerDependencies } from '../../dependencies';
2020
import { configFromEnv, configToRunContext } from '../../../core/config';
21-
import { ContractVerificationTestHandle, VerifiableContract } from './types';
22-
import { CaseContractDescription } from '../../../entities/types';
21+
import {
22+
VerificationTestHandle,
23+
ContractVerificationHandle,
24+
VerifiableContract,
25+
} from './types';
26+
import {
27+
CaseContractDescription,
28+
ContractMetadata,
29+
} from '../../../entities/types';
2330
import { TestPrinter } from '../types';
2431
import { readContractFromStore } from './readFromStore';
2532
import { filterContractsWithConfiguration } from './contractFilter';
2633

27-
type ContractVerifierHandle = {
28-
index: number;
34+
/**
35+
* Internal type for the connector to reference it's core verifiers.
36+
* This is the non-serialisable version of ContractVerificationHandle
37+
*/
38+
type InternalContractVerifierHandle = {
39+
/** The index of this contract within this verification run */
40+
contractIndex: number;
41+
/** The path to the contract file */
42+
filePath: string;
43+
/** The metadata from the contract file */
44+
metadata: ContractMetadata;
45+
/** The description from the contract file */
46+
description: CaseContractDescription;
47+
/** The verifier for this contract */
2948
verifier: ReadingCaseContract;
49+
/** The tests for this contract */
3050
tests: ContractVerificationTest[];
31-
filePath: string;
3251
};
3352

3453
type VerifierConstructorInfo<T extends AnyMockDescriptorType> = {
@@ -43,7 +62,7 @@ const getContractVerifierHandles = <T extends AnyMockDescriptorType>(
4362
context: DataContext,
4463
contractsToVerify: VerifiableContract[],
4564
constructorInfo: VerifierConstructorInfo<T>,
46-
) => {
65+
): InternalContractVerifierHandle[] => {
4766
if (contractsToVerify.length === 0) {
4867
throw new CaseConfigurationError(
4968
"No contracts were matched for verification. Try this run again with logLevel: 'debug' to find out more",
@@ -102,8 +121,10 @@ const getContractVerifierHandles = <T extends AnyMockDescriptorType>(
102121
const tests = contractVerifier.getTests(constructorInfo.invoker);
103122

104123
return {
105-
index,
124+
contractIndex: index,
106125
tests,
126+
metadata: verifiableContract.contract.contents.metadata,
127+
description: verifiableContract.contract.contents.description,
107128
verifier: contractVerifier,
108129
filePath: verifiableContract.contract.filePath,
109130
};
@@ -131,7 +152,7 @@ export class ContractVerifierConnector {
131152
*
132153
* @internal
133154
*/
134-
#contractVerificationHandles: ContractVerifierHandle[] | undefined;
155+
#contractVerificationHandles: InternalContractVerifierHandle[] | undefined;
135156

136157
constructor(
137158
userConfig: CaseConfig,
@@ -202,7 +223,7 @@ export class ContractVerifierConnector {
202223
string,
203224
(...args: unknown[]) => Promise<unknown>
204225
> = {},
205-
): ContractVerificationTestHandle[] {
226+
): ContractVerificationHandle[] {
206227
if (this.#contractVerificationHandles) {
207228
this.context.logger.maintainerDebug(
208229
'Invalid call to prepareVerification tests. Existing contents of this.#contractVerificationHandles:',
@@ -241,13 +262,19 @@ export class ContractVerifierConnector {
241262
'prepared verification handles set to:',
242263
this.#contractVerificationHandles,
243264
);
244-
return this.#contractVerificationHandles.flatMap((contractHandle) =>
245-
contractHandle.tests.map((testHandle) => ({
246-
testName: testHandle.testName,
247-
testIndex: testHandle.index,
248-
contractIndex: contractHandle.index,
265+
return this.#contractVerificationHandles.map(
266+
(contractHandle): ContractVerificationHandle => ({
267+
testHandles: contractHandle.tests.map((testHandle) => ({
268+
testName: testHandle.testName,
269+
testIndex: testHandle.index,
270+
contractIndex: contractHandle.contractIndex,
271+
filePath: contractHandle.filePath,
272+
})),
273+
contractIndex: contractHandle.contractIndex,
249274
filePath: contractHandle.filePath,
250-
})),
275+
metadata: contractHandle.metadata,
276+
description: contractHandle.description,
277+
}),
251278
);
252279
}
253280

@@ -272,7 +299,7 @@ export class ContractVerifierConnector {
272299
string,
273300
(...args: unknown[]) => Promise<unknown>
274301
> = {},
275-
): ContractVerificationTestHandle[] {
302+
): ContractVerificationHandle[] {
276303
return this.prepareMultiVerificationTests(
277304
[{ invoker, configOverride }],
278305
invokeableFns,
@@ -286,7 +313,7 @@ export class ContractVerifierConnector {
286313
* @returns a successful promise if the test ran. This doesn't necessarily
287314
* mean that the test passed.
288315
*/
289-
async runPreparedTest(test: ContractVerificationTestHandle): Promise<void> {
316+
async runPreparedTest(test: VerificationTestHandle): Promise<void> {
290317
const handles = this.#contractVerificationHandles;
291318
return Promise.resolve().then(() => {
292319
if (handles == null) {
@@ -312,6 +339,12 @@ export class ContractVerifierConnector {
312339
`The contract handle ${test.contractIndex} was undefined. This is probably a bug in the language DSL wrapper`,
313340
);
314341
}
342+
if (test.testIndex == null) {
343+
throw new CaseCoreError(
344+
`The provided testHandle didn't have a testIndex. This is probably a bug in the language DSL wrapper. Test handle was: ${JSON.stringify(test)}`,
345+
);
346+
}
347+
315348
const testHandle = contractHandle.tests[test.testIndex];
316349
this.context.logger.deepMaintainerDebug(
317350
'Run prepared test had testHandle',
@@ -394,7 +427,7 @@ export class ContractVerifierConnector {
394427
const contractVerifiers = this.#contractVerificationHandles.reduce<
395428
ReadingCaseContract[]
396429
>((acc, curr) => {
397-
acc[curr.index] = curr.verifier;
430+
acc[curr.contractIndex] = curr.verifier;
398431
return acc;
399432
}, []);
400433

packages/case-core/src/connectors/contract/ContractVerifierConnector/types.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
import { CaseConfig, ContractFileFromDisk } from '../../../core/types';
2+
import { CaseContractDescription } from '../../../entities/types';
23

3-
export interface ContractVerificationTestHandle {
4+
type Metadata = Record<string, string | Record<string, string>> & {
5+
_case: Record<string, string>;
6+
};
7+
8+
/**
9+
* Serialisable handle for a contract that is being verified.
10+
*
11+
*/
12+
export interface ContractVerificationHandle {
13+
contractIndex: number;
14+
filePath: string;
15+
metadata: Metadata;
16+
description: CaseContractDescription;
17+
testHandles: VerificationTestHandle[];
18+
}
19+
20+
/**
21+
* Serialisable handle for a verification test, returned by prepareVerificationTests,
22+
* and can be used to invoke this specific test later.
23+
*/
24+
export interface VerificationTestHandle {
425
testName: string;
526
testIndex: number;
627
contractIndex: number;

packages/case-core/src/index.http.client.spec.verify.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,15 @@ describe('Server verification', () => {
7777
},
7878
};
7979

80-
verifier
81-
.prepareVerificationTests({ stateHandlers })
82-
.forEach((verification) =>
80+
verifier.prepareVerificationTests({ stateHandlers }).forEach((contract) =>
81+
describe(`contract ${contract.filePath}`, () => {
8382
// eslint-disable-next-line jest/expect-expect
84-
it(`${verification.testName}`, () =>
85-
verifier.runPreparedTest(verification)),
86-
);
83+
contract.testHandles.forEach((testHandle) =>
84+
it(`${testHandle.testName}`, () =>
85+
verifier.runPreparedTest(testHandle)),
86+
);
87+
}),
88+
);
8789
},
8890
);
8991
});

packages/case-core/src/index.http.server.spec.verify.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,15 @@ verifyContract(
134134
},
135135
},
136136
})
137-
.forEach((verification) => {
138-
// eslint-disable-next-line jest/expect-expect
139-
it(`${verification.testName}`, () =>
140-
verifier.runPreparedTest(verification));
137+
.forEach((contract) => {
138+
// eslint-disable-next-line no-underscore-dangle
139+
describe(`Contract ${contract.metadata._case['hase']}`, () => {
140+
contract.testHandles.forEach((testHandle) =>
141+
// eslint-disable-next-line jest/expect-expect
142+
it(`${testHandle.testName}`, () =>
143+
verifier.runPreparedTest(testHandle)),
144+
);
145+
});
141146
});
142147
},
143148
);

packages/contract-case-jest/src/boundaries/jest/jest.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
/* eslint-disable jest/no-export */
33
import { ContractCaseDefiner } from '../../connectors/ContractDefiner.js';
44
import { ContractVerifier } from '../../connectors/ContractVerifier.js';
5-
import { ContractWriteSuccess } from '../../entities/index.js';
5+
import {
6+
ContractMetadata,
7+
ContractWriteSuccess,
8+
} from '../../entities/index.js';
69
import type {
710
ContractCaseJestConfig,
811
ContractCaseJestVerifierConfig,
@@ -54,6 +57,14 @@ export const defineContract = (
5457
});
5558
});
5659

60+
const renderHash = (metadata: ContractMetadata) =>
61+
'_case' in metadata &&
62+
typeof metadata['_case'] === 'object' &&
63+
'hash' in metadata['_case'] &&
64+
typeof metadata['_case']['hash'] === 'string'
65+
? ` (hash ${metadata['_case']['hash']})`
66+
: '';
67+
5768
/**
5869
* Convenience wrapper for verifying contracts. Calling this will generate all the tests
5970
* you need for
@@ -81,22 +92,21 @@ export const verifyContract = (
8192

8293
setupCallback(verifier);
8394

84-
const tests = verifier.prepareVerificationTests(config);
95+
const contracts = verifier.prepareVerificationTests(config);
96+
97+
contracts.forEach((contract) => {
98+
describe(`Contract between ${contract.description.consumerName} and ${contract.description.providerName} ${renderHash(contract.metadata)}`, () => {
99+
contract.testHandles.forEach((test) => {
100+
it(`${test.testName}`, () => verifier.runPreparedTest(test));
101+
});
102+
});
103+
// TODO: Determine whether Jest runs tests in order always, and if not, do something else here.
104+
it('Overall verification result', () =>
105+
verifier.closePreparedVerification(contract.contractIndex));
106+
});
85107

86-
tests.forEach((verificationTest) => {
87-
it(`${verificationTest.testName}`, () =>
88-
verifier.runPreparedTest(verificationTest));
108+
afterAll(() => {
109+
verificationComplete();
89110
});
90-
// TODO: Determine whether Jest runs tests in order always, and if not, do something else here.
91-
it('Overall verification result', () =>
92-
verifier.closePreparedVerification().then(
93-
() => {
94-
verificationComplete();
95-
},
96-
(e) => {
97-
verificationComplete();
98-
throw e;
99-
},
100-
));
101111
});
102112
};

packages/contract-case-jest/src/connectors/ContractVerifier.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import {
99
mapConfig,
1010
mapSuccessWithAny,
1111
mapInvokeableFunction,
12-
mapVerificationTestHandles,
12+
mapContractVerificationHandles,
1313
} from './case-boundary/index.js';
1414
import {
1515
ContractCaseConfigurationError,
1616
ContractCaseVerifierConfig,
1717
ContractDescription,
18+
VerificationHandle,
1819
VerificationTestHandle,
1920
versionString,
2021
} from '../entities/index.js';
@@ -95,9 +96,9 @@ export class ContractVerifier {
9596
*/
9697
prepareVerificationTests(
9798
configOverrides: Partial<ContractCaseVerifierConfig> = {},
98-
): VerificationTestHandle[] {
99+
): VerificationHandle[] {
99100
try {
100-
return mapVerificationTestHandles(
101+
return mapContractVerificationHandles(
101102
this.boundaryVerifier.prepareVerificationTests(
102103
mapConfig({
103104
...this.config,
@@ -131,9 +132,11 @@ export class ContractVerifier {
131132
}
132133
}
133134

134-
async closePreparedVerification(): Promise<void> {
135+
async closePreparedVerification(contractIndex: number): Promise<void> {
135136
try {
136-
mapSuccess(await this.boundaryVerifier.closeAllPreparedVerifications());
137+
mapSuccessWithAny(
138+
await this.boundaryVerifier.closePreparedVerification(contractIndex),
139+
);
137140
} catch (e) {
138141
throw errorReporter(e as Error);
139142
}

packages/contract-case-jest/src/connectors/case-boundary/mappers/boundaryResultToJs.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,15 @@ export const mapSuccess = (result: BoundaryResult): void => {
4545
case BoundaryResultTypeConstants.RESULT_SUCCESS:
4646
return;
4747
case BoundaryResultTypeConstants.RESULT_SUCCESS_HAS_MAP_PAYLOAD:
48+
throw new ContractCaseCoreError(
49+
'A void return from the core had a map payload',
50+
'mapSuccess',
51+
);
4852
case BoundaryResultTypeConstants.RESULT_SUCCESS_HAS_ANY_PAYLOAD:
49-
throw new Error("TODO: This shouldn't happen");
53+
throw new ContractCaseCoreError(
54+
'A void return from the core had an arbitrary payload instead',
55+
'mapSuccess',
56+
);
5057
default:
5158
throw new Error(`TODO: unexpected result type ${result.resultType}`);
5259
}
@@ -57,8 +64,15 @@ export const mapSuccessWithAny = <T>(result: BoundaryResult): T => {
5764
case BoundaryResultTypeConstants.RESULT_FAILURE:
5865
throw mapFailureToJsError(result as BoundaryFailure);
5966
case BoundaryResultTypeConstants.RESULT_SUCCESS:
67+
throw new ContractCaseCoreError(
68+
'An any return from the core had a void payload',
69+
'mapSuccess',
70+
);
6071
case BoundaryResultTypeConstants.RESULT_SUCCESS_HAS_MAP_PAYLOAD:
61-
throw new Error("TODO: This shouldn't happen");
72+
throw new ContractCaseCoreError(
73+
'An any return from the core had a map payload',
74+
'mapSuccess',
75+
);
6276
case BoundaryResultTypeConstants.RESULT_SUCCESS_HAS_ANY_PAYLOAD: {
6377
try {
6478
return JSON.parse((result as BoundarySuccessWithAny).payload) as T;

packages/contract-case-jest/src/connectors/case-boundary/mappers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export * from './boundaryResultToJs.js';
22
export * from './config/index.js';
33
export * from './invokeableFunction.js';
44
export * from './jsErrorToBoundary.js';
5-
export * from './verificationTestHandle.js';
5+
export * from './verificationHandles.js';

0 commit comments

Comments
 (0)