UNPKG

@contract-case/case-core

Version:

Core functionality for the ContractCase contract testing suite

191 lines 11.3 kB
"use strict"; 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