@contract-case/case-core
Version:
Core functionality for the ContractCase contract testing suite
191 lines • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContractVerifierConnector = void 0;
const case_plugin_base_1 = require("@contract-case/case-plugin-base");
const ReadingCaseContract_1 = require("../../core/ReadingCaseContract");
const dependencies_1 = require("../dependencies");
const config_1 = require("../../core/config");
const readContractFromStore = (config, reader) => {
if (config.contractFilename !== undefined &&
typeof config.contractFilename === 'string') {
return [reader.readContract(config.contractFilename)];
}
if (config.contractDir !== undefined &&
typeof config.contractDir === 'string') {
return reader.readContractsFromDir(config.contractDir);
}
throw new case_plugin_base_1.CaseConfigurationError('No contractFilename or contractDir specified. Must provide one of these so that Case can find the contract(s) to verify', 'DONT_ADD_LOCATION', 'INVALID_CONFIG');
};
class ContractVerifierConnector {
contracts;
config;
dependencies;
context;
parentVersions;
/**
* Internal links for the prepare / verify mode
*
* @internal
*/
#contractVerificationHandles;
constructor(userConfig, printer, parentVersions, dependencies = (0, dependencies_1.readerDependencies)(printer)) {
this.dependencies = dependencies;
this.parentVersions = parentVersions;
this.config = {
...dependencies.defaultConfig,
...(0, config_1.configFromEnv)(),
...userConfig,
};
this.context = // TODO: Extract constructDataContext to somewhere more DRY
(0, case_plugin_base_1.constructDataContext)(this.dependencies.makeLogger, this.dependencies.resultFormatter, {
...(0, config_1.configToRunContext)(this.config),
}, dependencies.defaultConfig, parentVersions);
const store = this.dependencies.makeContractStore(this.context);
this.contracts = readContractFromStore(this.config, store);
this.context.logger.deepMaintainerDebug('Constructed VerifierConnector');
}
filterContractsWithConfiguration(mergedConfig) {
if (typeof mergedConfig.providerName !== 'string') {
throw new case_plugin_base_1.CaseConfigurationError(`Must provide a providerName to verify (received '${mergedConfig.providerName}').`, 'DONT_ADD_LOCATION', 'INVALID_CONFIG');
}
this.context.logger.debug(`There are ${this.contracts.length} contracts loaded (this may include contracts that don't belong to this run)`);
this.contracts
.filter((item) => item.contents.description?.providerName !== mergedConfig.providerName)
.forEach((item) => {
this.context.logger.debug(`Skipping ${item.filePath} because it is not for the provider '${mergedConfig.providerName}' (It was for '${item.contents.description?.providerName}' instead)`);
});
const caseContractsForProvider = this.contracts.filter((item) => item.contents.description?.providerName === mergedConfig.providerName);
caseContractsForProvider
.filter((item) => typeof mergedConfig.consumerName !== 'undefined' &&
item.contents.description?.consumerName !== mergedConfig.consumerName)
.forEach((item) => {
this.context.logger.debug(`Skipping ${item.filePath} because it is not for the consumer '${mergedConfig.consumerName}' (It was for '${item.contents.description?.consumerName}' instead)`);
});
return caseContractsForProvider
.filter((item) => typeof mergedConfig.consumerName === 'undefined' ||
item.contents.description?.consumerName === mergedConfig.consumerName)
.map((contract) => ({ contract, config: mergedConfig }));
}
getAvailableContractDescriptions() {
return this.filterContractsWithConfiguration(this.config).map((verifiableContract) => verifiableContract.contract.contents.description);
}
/**
* This is the main entry point to verifying contract(s). It doesn't run the
* verification immediately,
* it returns a list of tests which can be called later with
* {@link ContractVerifierConnector#runPreparedTest}.
*
* @param invoker - The MultiTestInvoker for this run
* @param configOverride - any overridden config from when this runner was created
* @param invokeableFns - any invokeable functions that should be registered
* @returns
*/
prepareVerificationTests(invoker, configOverride = {}, invokeableFns = {}) {
const mergedConfig = { ...this.config, ...configOverride };
const contractsToVerify = this.filterContractsWithConfiguration(mergedConfig);
if (contractsToVerify.length === 0) {
throw new case_plugin_base_1.CaseConfigurationError("No contracts were matched for verification. Try this run again with logLevel: 'debug' to find out more", 'DONT_ADD_LOCATION');
}
if (mergedConfig.internals == null) {
throw new case_plugin_base_1.CaseCoreError('prepareVerification was called with no internals set - this is an error in the caller, probably the language specific wrapper');
}
if (contractsToVerify.length > 1) {
this.context.logger.debug(`*** There are ${contractsToVerify.length} contracts being prepared for verification ***`);
this.context.logger.debug(`Take note of the contract number in the log`);
}
this.#contractVerificationHandles = contractsToVerify.map((verifiableContract, index) => {
if (!verifiableContract.contract.contents?.description?.consumerName) {
this.context.logger.error(`Contract in file '${verifiableContract.contract.filePath}' appears to have no consumer name! It might not be a case contract`);
}
if (!verifiableContract.contract.contents?.description?.providerName) {
this.context.logger.error(`Contract in file '${verifiableContract.contract.filePath}' appears to have no provider name! It might not be a case contract`);
}
this.context.logger.debug(`*** Preparing contract: '${verifiableContract.contract.contents.description.consumerName}' -> '${verifiableContract.contract.contents.description.consumerName}'`);
this.context.logger.debug(`Contract File: ${verifiableContract.contract.filePath}`);
const contractVerifier = new ReadingCaseContract_1.ReadingCaseContract(verifiableContract.contract.contents, this.dependencies, {
...verifiableContract.config,
coreLogContextPrefix: contractsToVerify.length > 1 ? `Contract[${index}]` : '',
}, this.parentVersions);
Object.entries(invokeableFns).forEach(([key, value]) => {
contractVerifier.registerFunction(key, value);
});
const tests = contractVerifier.getTests(invoker);
return {
index,
tests,
verifier: contractVerifier,
filePath: verifiableContract.contract.filePath,
};
});
this.context.logger.deepMaintainerDebug('prepared verification handles set to:', this.#contractVerificationHandles);
return this.#contractVerificationHandles.flatMap((contractHandle) => contractHandle.tests.map((testHandle) => ({
testName: testHandle.testName,
testIndex: testHandle.index,
contractIndex: contractHandle.index,
filePath: contractHandle.filePath,
})));
}
/**
* Runs a prepared test returned by {@link prepareVerificationTests}.
*
* @param test - the test to run
* @returns a successful promise if the test ran. This doesn't necessarily
* mean that the test passed.
*/
async runPreparedTest(test) {
const handles = this.#contractVerificationHandles;
return Promise.resolve().then(() => {
if (handles == null) {
this.context.logger.deepMaintainerDebug('ERROR no contractVerificationHandles. Class was:', this);
throw new case_plugin_base_1.CaseCoreError('runPreparedTest was called before prepareVerificationTests. This is probably a bug in the language DSL wrapper');
}
const contractHandle = handles[test.contractIndex];
this.context.logger.deepMaintainerDebug('Run prepared test had contract handle', contractHandle);
if (contractHandle == null) {
this.context.logger.error('BUG: Run prepared test invoked incorrectly. See exception for details. The contractVerificationHandles object is:', handles);
throw new case_plugin_base_1.CaseCoreError(`The contract handle ${test.contractIndex} was undefined. This is probably a bug in the language DSL wrapper`);
}
const testHandle = contractHandle.tests[test.testIndex];
this.context.logger.deepMaintainerDebug('Run prepared test had testHandle', testHandle);
if (testHandle == null) {
this.context.logger.error('BUG: Run prepared test invoked incorrectly. See exception for details. The contractVerificationHandles object is:', handles);
throw new case_plugin_base_1.CaseCoreError(`The testHandle ${test.testIndex} was undefined. This is probably a bug in the language DSL wrapper`);
}
return testHandle.runTest();
});
}
/**
* Closes a verification
*
* @returns a successful promise if the verification closed successfully
*/
async closePreparedVerification() {
return Promise.resolve().then(() => {
if (this.#contractVerificationHandles == null) {
this.context.logger.maintainerDebug("Closing contract verifications, but they weren't prepared - assuming closed.");
// We don't need to close tests run with runVerification
return Promise.resolve();
}
const contractVerifiers = this.#contractVerificationHandles.reduce((acc, curr) => {
acc[curr.index] = curr.verifier;
return acc;
}, []);
this.context.logger.maintainerDebug('Closing contract verifications', contractVerifiers);
return Promise.allSettled(contractVerifiers.map((v) => Promise.resolve().then(() => v.endRecord()))).then((results) => {
const failures = results
.filter((result) => result.status === 'rejected')
.map(({ reason }) => reason);
if (failures.length > 0) {
this.context.logger.error(`There were failures verifying ${failures.length} contracts`);
failures.forEach((failure) => {
this.context.logger.error(`${failure.name ? `${failure.name}: ` : ''} ${failure.message}`);
});
this.context.logger.error(`Throwing only the first error`);
throw failures[0];
}
});
});
}
}
exports.ContractVerifierConnector = ContractVerifierConnector;
//# sourceMappingURL=ContractVerifierConnector.js.map