diff --git a/package.json b/package.json index bb82fdab5a..0614687a3b 100644 --- a/package.json +++ b/package.json @@ -22,33 +22,33 @@ "osls": "./bin/serverless.js" }, "dependencies": { - "@aws-sdk/client-api-gateway": "^3.588.0", - "@aws-sdk/client-apigatewayv2": "^3.588.0", - "@aws-sdk/client-cloudformation": "^3.588.0", - "@aws-sdk/client-cloudwatch": "^3.588.0", - "@aws-sdk/client-cloudwatch-logs": "^3.588.0", - "@aws-sdk/client-cognito-identity-provider": "^3.588.0", - "@aws-sdk/client-dynamodb": "^3.588.0", - "@aws-sdk/client-ecr": "^3.588.0", - "@aws-sdk/client-eventbridge": "^3.588.0", - "@aws-sdk/client-iam": "^3.588.0", - "@aws-sdk/client-iot": "^3.588.0", - "@aws-sdk/client-iot-data-plane": "^3.588.0", - "@aws-sdk/client-kinesis": "^3.588.0", - "@aws-sdk/client-lambda": "^3.588.0", - "@aws-sdk/client-s3": "^3.588.0", - "@aws-sdk/client-sns": "^3.588.0", - "@aws-sdk/client-sqs": "^3.588.0", - "@aws-sdk/client-ssm": "^3.588.0", - "@aws-sdk/client-sts": "^3.588.0", - "@aws-sdk/lib-dynamodb": "^3.588.0", - "@aws-sdk/lib-storage": "^3.588.0", - "@aws-sdk/credential-providers": "^3.588.0", + "@aws-sdk/client-api-gateway": "^3.975.0", + "@aws-sdk/client-apigatewayv2": "^3.975.0", + "@aws-sdk/client-cloudformation": "^3.975.0", + "@aws-sdk/client-cloudwatch": "^3.975.0", + "@aws-sdk/client-cloudwatch-logs": "^3.975.0", + "@aws-sdk/client-cognito-identity-provider": "^3.975.0", + "@aws-sdk/client-dynamodb": "^3.975.0", + "@aws-sdk/client-ecr": "^3.975.0", + "@aws-sdk/client-eventbridge": "^3.975.0", + "@aws-sdk/client-iam": "^3.975.0", + "@aws-sdk/client-iot": "^3.975.0", + "@aws-sdk/client-iot-data-plane": "^3.975.0", + "@aws-sdk/client-kinesis": "^3.975.0", + "@aws-sdk/client-lambda": "^3.975.0", + "@aws-sdk/client-s3": "^3.975.0", + "@aws-sdk/client-sns": "^3.975.0", + "@aws-sdk/client-sqs": "^3.975.0", + "@aws-sdk/client-ssm": "^3.975.0", + "@aws-sdk/client-sts": "^3.975.0", + "@aws-sdk/credential-providers": "^3.975.0", + "@aws-sdk/lib-dynamodb": "^3.975.0", + "@aws-sdk/lib-storage": "^3.975.0", "@serverless/utils": "^6.13.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "archiver": "^7.0.1", - "aws-sdk": "^2.1692.0", + "aws-sdk": "^2.1693.0", "bluebird": "^3.7.2", "cachedir": "^2.3.0", "chalk": "^4.1.2", @@ -98,7 +98,6 @@ }, "devDependencies": { "@serverless/eslint-config": "^5.1.0", - "@serverless/test": "^11.1.1", "adm-zip": "^0.5.10", "aws4": "^1.12.0", "chai": "^4.3.7", @@ -108,13 +107,14 @@ "git-list-updated": "^1.2.1", "husky": "^4.3.8", "jszip": "^3.10.1", - "lint-staged": "^13.2.2", + "lint-staged": "^16.2.7", "log": "^6.3.1", "log-node": "^8.0.3", - "mocha": "^9.2.2", + "minimist": "^1.2.8", + "mocha": "^11.7.5", "mock-require": "^3.0.3", "ncjsm": "^4.3.2", - "pkg": "^5.8.1", + "p-limit": "^3.1.0", "prettier": "^2.8.8", "proxyquire": "^2.1.3", "semver-regex": "^3.1.4", @@ -171,10 +171,10 @@ "mocha": { "require": [ "./test/mocha-patch", - "@serverless/test/setup/log", - "@serverless/test/setup/mock-homedir", - "@serverless/test/setup/mock-cwd", - "@serverless/test/setup/restore-env" + "./test/lib/setup/log", + "./test/lib/setup/mock-homedir", + "./test/lib/setup/mock-cwd", + "./test/lib/setup/restore-env" ], "timeout": 60000 }, @@ -188,9 +188,9 @@ }, "scripts": { "integration-test-cleanup": "node test/utils/aws-cleanup.js", - "integration-test-run-all": "mocha-isolated --pass-through-aws-creds --skip-fs-cleanup-check --max-workers=20 \"test/integration/**/*.test.js\"", + "integration-test-run-all": "node test/lib/bin/mocha-isolated.js --pass-through-aws-creds --skip-fs-cleanup-check --max-workers=20 \"test/integration/**/*.test.js\"", "integration-test-run-basic": "mocha test/integration-basic.test.js", - "integration-test-run-package": "mocha-isolated --skip-fs-cleanup-check test/integration-package/**/*.tests.js", + "integration-test-run-package": "node test/lib/bin/mocha-isolated.js --skip-fs-cleanup-check test/integration-package/**/*.tests.js", "integration-test-setup": "node ./scripts/test/integration-setup/index.js", "integration-test-teardown": "node ./scripts/test/integration-teardown.js", "lint": "eslint .", @@ -203,7 +203,7 @@ "prettify:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml --base=main -- prettier --write", "test": "mocha \"test/unit/**/*.test.js\"", "test:ci": "npm run prettier-check:updated && npm run lint:updated && npm run test:isolated", - "test:isolated": "mocha-isolated \"test/unit/**/*.test.js\"" + "test:isolated": "node test/lib/bin/mocha-isolated.js \"test/unit/**/*.test.js\"" }, "engines": { "node": ">=12.0" diff --git a/scripts/test/integration-setup/index.js b/scripts/test/integration-setup/index.js index d3bedd1485..4ef60edb8e 100755 --- a/scripts/test/integration-setup/index.js +++ b/scripts/test/integration-setup/index.js @@ -6,7 +6,7 @@ require('essentials'); require('log-node')(); const log = require('log').get('serverless'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../../test/lib/aws-request'); const fsp = require('fs').promises; const path = require('path'); const CloudFormationService = require('aws-sdk').CloudFormation; diff --git a/scripts/test/integration-teardown.js b/scripts/test/integration-teardown.js index 5a88331c97..9bc63b7ff6 100755 --- a/scripts/test/integration-teardown.js +++ b/scripts/test/integration-teardown.js @@ -6,7 +6,7 @@ require('essentials'); require('log-node')(); const log = require('log').get('serverless'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../test/lib/aws-request'); const CloudFormationService = require('aws-sdk').CloudFormation; const EC2Service = require('aws-sdk').EC2; const KafkaService = require('aws-sdk').Kafka; diff --git a/test/README.md b/test/README.md index 2dfd3e2a81..0d22ccd998 100644 --- a/test/README.md +++ b/test/README.md @@ -15,7 +15,7 @@ npm test All new tests should be configured with help of [runServerless](./utils/run-serverless.js) util - it's the only way to test functionality against completely intialized `serverless` instance, and it's the only scenario that reflects real world usage. -Check documentation of `runServerless` at [@serverless/test/docs/run-serverless](https://github.com/serverless/test/blob/main/docs/run-serverless.md#run-serverless). Note that `runServerless` as configured at `./utils/run-serverless.js` supports two additional options (`fixture` and `configExt`), which provides out of a box setup to run _Serverless_ instance against prepared fixture with eventually extended service configuration +The `runServerless` util (inlined from @serverless/test) is configured at `./utils/run-serverless.js` and supports two additional options (`fixture` and `configExt`), which provides out of a box setup to run _Serverless_ instance against prepared fixture with eventually extended service configuration As `runServerless` tests are expensive, it's good to ensure a _minimal_ count of `runServerless` runs to test given scope of problems. Ideally with one service example we should cover most of the test cases we can (good example of such approach is [ALB health check tests](https://github.com/serverless/serverless/blob/80e70e7affd54418361c4d54bdef1561af6b8826/lib/plugins/aws/package/compile/events/alb/lib/healthCheck.test.js#L18-L127)) diff --git a/test/fixtures/cli/index.js b/test/fixtures/cli/index.js index 467a7bf4b0..00716b00ac 100644 --- a/test/fixtures/cli/index.js +++ b/test/fixtures/cli/index.js @@ -1,3 +1,3 @@ 'use strict'; -module.exports = require('@serverless/test/setup-fixtures-engine')(__dirname); +module.exports = require('../../lib/setup-fixtures-engine')(__dirname); diff --git a/test/fixtures/programmatic/index.js b/test/fixtures/programmatic/index.js index 467a7bf4b0..00716b00ac 100644 --- a/test/fixtures/programmatic/index.js +++ b/test/fixtures/programmatic/index.js @@ -1,3 +1,3 @@ 'use strict'; -module.exports = require('@serverless/test/setup-fixtures-engine')(__dirname); +module.exports = require('../../lib/setup-fixtures-engine')(__dirname); diff --git a/test/integration-basic.test.js b/test/integration-basic.test.js index d0fea7fdd7..b03af56192 100644 --- a/test/integration-basic.test.js +++ b/test/integration-basic.test.js @@ -7,9 +7,9 @@ const stripAnsi = require('strip-ansi'); const { expect } = require('chai'); const log = require('log').get('serverless:test'); const spawn = require('child-process-ext/spawn'); -const resolveAwsEnv = require('@serverless/test/resolve-aws-env'); -const hasFailed = require('@serverless/test/has-failed'); -const awsRequest = require('@serverless/test/aws-request'); +const resolveAwsEnv = require('./lib/resolve-aws-env'); +const hasFailed = require('./lib/has-failed'); +const awsRequest = require('./lib/aws-request'); const CloudFormationService = require('aws-sdk').CloudFormation; const { getTmpDirPath } = require('./utils/fs'); diff --git a/test/integration/aws/api-gateway-external.test.js b/test/integration/aws/api-gateway-external.test.js index af9ae7af6a..f0749bbc90 100644 --- a/test/integration/aws/api-gateway-external.test.js +++ b/test/integration/aws/api-gateway-external.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const log = require('log').get('serverless:test'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../lib/aws-request'); const CloudFormationService = require('aws-sdk').CloudFormation; const fixtures = require('../../fixtures/programmatic'); diff --git a/test/integration/aws/api-gateway.test.js b/test/integration/aws/api-gateway.test.js index 9c5e619499..4f2f1755bf 100644 --- a/test/integration/aws/api-gateway.test.js +++ b/test/integration/aws/api-gateway.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const log = require('log').get('serverless:test'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../lib/aws-request'); const CloudFormationService = require('aws-sdk').CloudFormation; const fixtures = require('../../fixtures/programmatic'); diff --git a/test/integration/aws/cognito-user-pool.test.js b/test/integration/aws/cognito-user-pool.test.js index 46d459915d..a981d69710 100644 --- a/test/integration/aws/cognito-user-pool.test.js +++ b/test/integration/aws/cognito-user-pool.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const log = require('log').get('serverless:test'); -const hasFailed = require('@serverless/test/has-failed'); +const hasFailed = require('../../lib/has-failed'); const fixtures = require('../../fixtures/programmatic'); const { diff --git a/test/integration/aws/custom-deployment-bucket.test.js b/test/integration/aws/custom-deployment-bucket.test.js index 3654a14af5..806ebb1a98 100644 --- a/test/integration/aws/custom-deployment-bucket.test.js +++ b/test/integration/aws/custom-deployment-bucket.test.js @@ -3,7 +3,7 @@ const uuid = require('uuid'); const { expect } = require('chai'); const fixtures = require('../../fixtures/programmatic'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../lib/aws-request'); const S3Service = require('aws-sdk').S3; const { deployService, removeService } = require('../../utils/integration'); const { createBucket, deleteBucket } = require('../../utils/s3'); diff --git a/test/integration/aws/function-url.test.js b/test/integration/aws/function-url.test.js index e66f6a1ff3..7553e7f673 100644 --- a/test/integration/aws/function-url.test.js +++ b/test/integration/aws/function-url.test.js @@ -1,7 +1,7 @@ 'use strict'; const { expect } = require('chai'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../lib/aws-request'); const CloudFormationService = require('aws-sdk').CloudFormation; const fixtures = require('../../fixtures/programmatic'); const aws4 = require('aws4'); diff --git a/test/integration/aws/function.test.js b/test/integration/aws/function.test.js index c200b08dbd..94f70c335b 100644 --- a/test/integration/aws/function.test.js +++ b/test/integration/aws/function.test.js @@ -1,7 +1,7 @@ 'use strict'; const { expect } = require('chai'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../lib/aws-request'); const LambdaService = require('aws-sdk').Lambda; const fixtures = require('../../fixtures/programmatic'); const { confirmCloudWatchLogs } = require('../../utils/misc'); diff --git a/test/integration/aws/http-api.test.js b/test/integration/aws/http-api.test.js index ff8ce12134..841f6568c2 100644 --- a/test/integration/aws/http-api.test.js +++ b/test/integration/aws/http-api.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const log = require('log').get('serverless:test'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../lib/aws-request'); const CloudFormationService = require('aws-sdk').CloudFormation; const CognitoIdentityServiceProviderService = require('aws-sdk').CognitoIdentityServiceProvider; const ApiGatewayV2Service = require('aws-sdk').ApiGatewayV2; diff --git a/test/integration/aws/infra-dependent/active-mq.test.js b/test/integration/aws/infra-dependent/active-mq.test.js index ed0250b90e..35f39cd436 100644 --- a/test/integration/aws/infra-dependent/active-mq.test.js +++ b/test/integration/aws/infra-dependent/active-mq.test.js @@ -10,7 +10,7 @@ const { SHARED_INFRA_TESTS_ACTIVE_MQ_CREDENTIALS_NAME, } = require('../../../utils/cloudformation'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../../lib/aws-request'); const LambdaService = require('aws-sdk').Lambda; const MQService = require('aws-sdk').MQ; const SecretsManagerService = require('aws-sdk').SecretsManager; diff --git a/test/integration/aws/infra-dependent/file-system-config.test.js b/test/integration/aws/infra-dependent/file-system-config.test.js index 0359f69bb5..422bfa6adb 100644 --- a/test/integration/aws/infra-dependent/file-system-config.test.js +++ b/test/integration/aws/infra-dependent/file-system-config.test.js @@ -4,7 +4,7 @@ const { expect } = require('chai'); const log = require('log').get('serverless:test'); const fixtures = require('../../../fixtures/programmatic'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../../lib/aws-request'); const LambdaService = require('aws-sdk').Lambda; const crypto = require('crypto'); const { deployService, removeService } = require('../../../utils/integration'); diff --git a/test/integration/aws/infra-dependent/msk.test.js b/test/integration/aws/infra-dependent/msk.test.js index 4fbda0e681..4a691e86c5 100644 --- a/test/integration/aws/infra-dependent/msk.test.js +++ b/test/integration/aws/infra-dependent/msk.test.js @@ -9,7 +9,7 @@ const { getDependencyStackOutputMap, } = require('../../../utils/cloudformation'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../../lib/aws-request'); const LambdaService = require('aws-sdk').Lambda; const KafkaService = require('aws-sdk').Kafka; const crypto = require('crypto'); diff --git a/test/integration/aws/infra-dependent/rabbit-mq.test.js b/test/integration/aws/infra-dependent/rabbit-mq.test.js index e3b7dc78ed..97770e4abb 100644 --- a/test/integration/aws/infra-dependent/rabbit-mq.test.js +++ b/test/integration/aws/infra-dependent/rabbit-mq.test.js @@ -10,7 +10,7 @@ const { SHARED_INFRA_TESTS_RABBITMQ_CREDENTIALS_NAME, } = require('../../../utils/cloudformation'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../../lib/aws-request'); const LambdaService = require('aws-sdk').Lambda; const MQService = require('aws-sdk').MQ; const SecretsManagerService = require('aws-sdk').SecretsManager; diff --git a/test/integration/aws/iot-fleet-provisioning.test.js b/test/integration/aws/iot-fleet-provisioning.test.js index f9cbe457c2..1f8c922cd2 100644 --- a/test/integration/aws/iot-fleet-provisioning.test.js +++ b/test/integration/aws/iot-fleet-provisioning.test.js @@ -1,10 +1,10 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../lib/aws-request'); const CloudFormationService = require('aws-sdk').CloudFormation; const IotService = require('aws-sdk').Iot; const LambdaService = require('aws-sdk').Lambda; -const hasFailed = require('@serverless/test/has-failed'); +const hasFailed = require('../../lib/has-failed'); const { expect } = require('chai'); const fixtures = require('../../fixtures/programmatic'); const { deployService, removeService } = require('../../utils/integration'); diff --git a/test/integration/aws/sqs.test.js b/test/integration/aws/sqs.test.js index 9983777e69..766f32e8a8 100644 --- a/test/integration/aws/sqs.test.js +++ b/test/integration/aws/sqs.test.js @@ -1,7 +1,7 @@ 'use strict'; const { expect } = require('chai'); -const hasFailed = require('@serverless/test/has-failed'); +const hasFailed = require('../../lib/has-failed'); const log = require('log').get('serverless:test'); const fixtures = require('../../fixtures/programmatic'); diff --git a/test/integration/aws/websocket.test.js b/test/integration/aws/websocket.test.js index 326e4f5130..4fb11c62a0 100644 --- a/test/integration/aws/websocket.test.js +++ b/test/integration/aws/websocket.test.js @@ -2,7 +2,7 @@ const WebSocket = require('ws'); const { expect } = require('chai'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../../lib/aws-request'); const CloudFormationService = require('aws-sdk').CloudFormation; const log = require('log').get('serverless:test'); const wait = require('timers-ext/promise/sleep'); diff --git a/test/lib/aws-request.js b/test/lib/aws-request.js new file mode 100644 index 0000000000..453fb29bee --- /dev/null +++ b/test/lib/aws-request.js @@ -0,0 +1,60 @@ +'use strict'; + +const isPlainObject = require('type/plain-object/is'); +const isThenable = require('type/thenable/is'); +const ensureConstructor = require('type/constructor/ensure'); +const ensurePlainObject = require('type/plain-object/ensure'); +const memoizeWeak = require('memoizee/weak'); +const awsLog = require('log').get('aws'); +const wait = require('timers-ext/promise/sleep'); + +const getClientInstance = memoizeWeak( + (Client, options) => { + const params = { region: 'us-east-1', ...options }; + return new Client(params); + }, + { normalizer: (ignore, [options]) => JSON.stringify(options) } +); + +const resolveClientData = (clientOrClientConfig) => { + if (isPlainObject(clientOrClientConfig)) { + return [ + ensureConstructor(clientOrClientConfig.client), + ensurePlainObject(clientOrClientConfig.params, { default: {} }), + ]; + } + return [ensureConstructor(clientOrClientConfig), {}]; +}; + +let lastAwsRequestId = 0; +module.exports = function awsRequest(clientOrClientConfig, method, ...args) { + const requestId = ++lastAwsRequestId; + awsLog.debug('[%d] %O %s %O', requestId, clientOrClientConfig, method, args); + const instance = getClientInstance(...resolveClientData(clientOrClientConfig)); + const response = instance[method](...args); + const promise = isThenable(response) ? response : response.promise(); + return promise.then( + (result) => { + awsLog.debug('[%d] %O', requestId, result); + return result; + }, + (error) => { + awsLog.debug('[%d] %O', requestId, error); + const shouldRetry = (() => { + if (error.statusCode === 403) return false; + if (error.retryable) return true; + if (error.Reason === 'CallerRateLimitExceeded') return true; + if (error.message.includes('Rate exceeded')) return true; + if (error.message.includes('Too Many Requests')) return true; + return false; + })(); + if (shouldRetry) { + awsLog.debug('[%d] retry', requestId); + return wait(4000 + Math.random() * 3000).then(() => + awsRequest(clientOrClientConfig, method, ...args) + ); + } + throw error; + } + ); +}; diff --git a/test/lib/bin/mocha-isolated.js b/test/lib/bin/mocha-isolated.js new file mode 100755 index 0000000000..2ae05f9200 --- /dev/null +++ b/test/lib/bin/mocha-isolated.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node + +'use strict'; + +require('essentials'); + +const chalk = require('chalk'); +const argv = require('minimist')(process.argv.slice(2), { + boolean: [ + 'bail', + 'help', + 'pass-through-aws-creds', + 'recursive', + 'skip-fs-cleanup-check', + 'version', + ], + string: ['require'], + alias: { 'help': 'h', 'version': 'v', 'max-workers': 'w', 'bail': 'b' }, + unknown: (arg) => { + if (arg[0] !== '-') return; + process.stdout.write(chalk.red.bold(`Unrecognized option ${arg}\n\n`)); + process.exit(1); + }, +}); + +const usage = `Usage: mocha-isolated [...] [...] + +Runs tests with mocha, each test is run in individual Node.js process + +Options: + + --help, -h Show this message + --version, -v Print the version and exit + + --bail, -b Bail gently after first approached test fail + --pass-through-aws-creds Pass through AWS env credentials + --max-workers -w Maximum allowed number of workers for concurrent run + --recursive Look for tests in subdirectories + --skip-fs-cleanup-check Do not check on modified files (allows parallel runs) + --require Mocha's "require" option (passed through to Mocha) +`; + +if (argv.help) { + process.stdout.write(usage); + return; +} + +if (argv.version) { + process.stdout.write(`${require('../../../package').version}\n`); + return; +} + +const spawn = require('child-process-ext/spawn'); +const pLimit = require('p-limit'); +const mochaCollectFiles = require('mocha/lib/cli/collect-files'); +const resolveEnv = require('../resolve-env'); +const resolveAwsEnv = require('../resolve-aws-env'); + +const filePatterns = argv._; +if (!filePatterns.length) filePatterns.push('!(node_modules)/**/*.test.js', '*.test.js'); + +const resolveGitStatus = () => + spawn('git', ['status', '--porcelain']).then( + ({ stdoutBuffer }) => String(stdoutBuffer), + (error) => { + process.stdout.write(error.stdBuffer); + throw error; + } + ); + +const initialGitStatusDeferred = !argv['skip-fs-cleanup-check'] ? resolveGitStatus() : null; + +const initialSetupDeferred = !argv['skip-fs-cleanup-check'] + ? initialGitStatusDeferred + : Promise.resolve(); + +const cwdPathLength = process.cwd().length + 1; +const collectResult = mochaCollectFiles({ + ignore: [], + extension: ['js'], + file: [], + recursive: argv.recursive, + spec: filePatterns, +}); +// Mocha 11+ returns {files: [], unmatchedFiles: []}, earlier versions return array directly +const filesList = collectResult.files || collectResult; +const paths = filesList.map((filename) => filename.slice(cwdPathLength)); + +if (!paths.length) { + process.stdout.write(chalk.red.bold('No test files matched\n\n')); + process.exit(1); +} + +const processesCount = (() => { + if (!argv['skip-fs-cleanup-check']) return 1; + const forced = Number(argv.w); + if (forced > 0) return Math.min(forced, paths.length); + return Math.min(Math.max(require('os').cpus().length - 1, 1), paths.length); +})(); + +const isMultiProcessRun = processesCount > 1; + +const { ongoingPaths, cliFooter } = (() => { + if (!isMultiProcessRun) return {}; + return { ongoingPaths: new Set(), cliFooter: require('cli-progress-footer')() }; +})(); + +const failed = []; +process.on('exit', () => { + if (!failed.length) return; + process.stdout.write('\n'); + for (const testPath of failed) process.stdout.write(chalk.red.bold(`${testPath} failed\n`)); +}); + +let shouldAbort = false; +let pendingCount = paths.length; +const ongoingProcesses = new Set(); +const run = (path) => { + --pendingCount; + if (shouldAbort) return null; + + const env = argv['pass-through-aws-creds'] ? resolveAwsEnv() : resolveEnv(); + env.FORCE_COLOR = '1'; + const mochaArgs = []; + if (argv.require) mochaArgs.push('--require', argv.require); + const testPromise = spawn('node', [require.resolve('mocha/bin/_mocha'), ...mochaArgs, path], { + stdio: isMultiProcessRun ? null : 'inherit', + env, + }); + + if (isMultiProcessRun) { + ongoingPaths.add(path); + cliFooter.updateProgress(Array.from(ongoingPaths)); + ongoingProcesses.add(testPromise); + } + + const onFinally = (() => { + if (isMultiProcessRun) { + return ({ stdBuffer }) => { + ongoingProcesses.delete(testPromise); + ongoingPaths.delete(path); + cliFooter.updateProgress(Array.from(ongoingPaths)); + if (!pendingCount && !ongoingProcesses.size) return Promise.resolve(); + process.stdout.write(stdBuffer); + if (!pendingCount && ongoingProcesses.size === 1) { + cliFooter.updateProgress(); + const lastProcess = ongoingProcesses[Symbol.iterator]().next().value; + process.stdout.write(lastProcess.stdBuffer); + lastProcess.std.pipe(process.stdout); + } + return Promise.resolve(); + }; + } + if (argv['skip-fs-cleanup-check']) return () => Promise.resolve(); + return () => + Promise.all([initialGitStatusDeferred, resolveGitStatus()]).then( + ([initialStatus, currentStatus]) => { + if (initialStatus !== currentStatus) { + process.stdout.write( + chalk.red.bold(`${path} didn't clean created temporary files\n\n`) + ); + failed.push(path); + process.exitCode = 1; + shouldAbort = true; + } + } + ); + })(); + + return testPromise.then(onFinally, (error) => + onFinally(error).then(() => { + failed.push(path); + process.stdout.write(`${chalk.red.bold(error.message)}\n\n`); + process.stdout.write(`${chalk.red.bold(`${path} failed`)}\n\n`); + process.exitCode = 1; + if (argv.bail) shouldAbort = true; + }) + ); +}; + +const limit = pLimit(processesCount); +initialSetupDeferred.then(() => Promise.all(paths.map((path) => limit(() => run(path))))); diff --git a/test/lib/configure-aws-request-stub.js b/test/lib/configure-aws-request-stub.js new file mode 100644 index 0000000000..3cc829701c --- /dev/null +++ b/test/lib/configure-aws-request-stub.js @@ -0,0 +1,27 @@ +'use strict'; + +const ensureObject = require('type/object/ensure'); +const ensurePlainObject = require('type/plain-object/ensure'); +const ensureFunction = require('type/plain-function/ensure'); +const sinon = require('sinon'); + +module.exports = (provider, config) => { + ensureObject(provider); + ensureFunction(provider.request); + ensurePlainObject(config); + + if (provider.request.restore) provider.request.restore(); + + return sinon.stub(provider, 'request').callsFake( + (service, methodName, ...args) => + new Promise((resolve, reject) => { + if (!config[service] || !config[service][methodName]) { + reject(new Error(`Missing AWS request stub configuration for ${service}.${methodName}`)); + return; + } + + const method = config[service][methodName]; + resolve(typeof method === 'function' ? method(...args) : method); + }) + ); +}; diff --git a/test/lib/configure-inquirer-stub.js b/test/lib/configure-inquirer-stub.js new file mode 100644 index 0000000000..552f2d4945 --- /dev/null +++ b/test/lib/configure-inquirer-stub.js @@ -0,0 +1,63 @@ +'use strict'; + +const sinon = require('sinon'); + +const validatedTypes = new Set(['input', 'password']); + +module.exports = (inquirer, config) => { + const resolveAnswer = (promptConfig) => { + return new Promise((resolve) => { + const configType = promptConfig.type || 'input'; + const questions = config[configType]; + if (!questions) throw new Error(`Unexpected config type: ${configType}`); + let answer = questions[promptConfig.name]; + if (answer == null) throw new Error(`Unexpected config name: ${promptConfig.name}`); + if (configType === 'list') { + if ( + !promptConfig.choices.some((choice) => { + if (typeof choice === 'string') return choice === answer; + if (choice.name === answer || choice.value === answer) { + answer = choice.value; + return true; + } + return false; + }) + ) { + throw new Error(`Unsupported list result: ${answer}`); + } + } + + resolve( + new Promise((resolveValidation) => { + if (!validatedTypes.has(promptConfig.type)) return resolveValidation(true); + if (!promptConfig.validate) return resolveValidation(true); + return resolveValidation(promptConfig.validate(answer)); + }).then((validationResult) => { + if (validationResult !== true) { + throw Object.assign(new Error(validationResult), { code: 'INVALID_ANSWER' }); + } + return { [promptConfig.name]: answer }; + }) + ); + }); + }; + + if (inquirer.prompt.restore) inquirer.prompt.restore(); + if (inquirer.createPromptModule.restore) inquirer.createPromptModule.restore(); + + sinon.stub(inquirer, 'prompt').callsFake((promptConfig) => { + if (!Array.isArray(promptConfig)) return resolveAnswer(promptConfig); + const result = {}; + return promptConfig.reduce( + (previusPromptDeferred, nextPromptConfig) => + previusPromptDeferred.then((answer) => { + Object.assign(result, answer); + return resolveAnswer(nextPromptConfig); + }), + Promise.resolve({}) + ); + }); + + sinon.stub(inquirer, 'createPromptModule').callsFake(() => inquirer.prompt); + return inquirer; +}; diff --git a/test/lib/disable-serverless-stats-requests.js b/test/lib/disable-serverless-stats-requests.js new file mode 100644 index 0000000000..92057bd7fa --- /dev/null +++ b/test/lib/disable-serverless-stats-requests.js @@ -0,0 +1,11 @@ +// Warning: Needs to be required before any serverless modules are required! + +'use strict'; + +const path = require('path'); + +module.exports = (serverlessPath) => { + const modulePath = path.join(serverlessPath, 'lib/utils/telemetry/are-disabled'); + // Ensure no tracking during tests run + require.cache[require.resolve(modulePath)] = { exports: true }; +}; diff --git a/test/lib/has-failed.js b/test/lib/has-failed.js new file mode 100644 index 0000000000..f4affad1be --- /dev/null +++ b/test/lib/has-failed.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = (suite) => { + if (suite.tests.some((test) => test.state === 'failed')) return true; + return suite.suites.some(module.exports); +}; diff --git a/test/lib/lib/private/rm-tmp-dir-ignorable-error-codes.js b/test/lib/lib/private/rm-tmp-dir-ignorable-error-codes.js new file mode 100644 index 0000000000..1fbcf35a5d --- /dev/null +++ b/test/lib/lib/private/rm-tmp-dir-ignorable-error-codes.js @@ -0,0 +1,6 @@ +'use strict'; + +// Removal temporary directory reported occassional crashes on Winodws (in CI) +// It's just a cleanup operation, so failure is safe to ignore +// Exported set, lists all error codes we recognize as safe to ignore +module.exports = new Set(['EBUSY', 'EPERM']); diff --git a/test/lib/observe-output.js b/test/lib/observe-output.js new file mode 100644 index 0000000000..3adb69b15e --- /dev/null +++ b/test/lib/observe-output.js @@ -0,0 +1,33 @@ +'use strict'; + +const isThenable = require('type/thenable/is'); +const { emitter: outputEmitter } = require('@serverless/utils/lib/log/get-output-reporter'); +const joinTextTokens = require('@serverless/utils/lib/log/join-text-tokens'); + +module.exports = (callback) => { + let output = ''; + const outputListener = ({ mode, textTokens }) => { + if (mode === 'text') output += joinTextTokens(textTokens); + }; + outputEmitter.on('write', outputListener); + const cleanup = () => outputEmitter.off('write', outputListener); + const propagateException = (error) => { + error.output = output; + throw error; + }; + const result = (() => { + try { + return callback(); + } catch (error) { + cleanup(); + return propagateException(error); + } + })(); + if (!isThenable(result)) { + cleanup(); + return output; + } + return Promise.resolve(result) + .then(() => output, propagateException) + .finally(cleanup); +}; diff --git a/test/lib/process-tmp-dir.js b/test/lib/process-tmp-dir.js new file mode 100644 index 0000000000..65e4fe2d31 --- /dev/null +++ b/test/lib/process-tmp-dir.js @@ -0,0 +1,36 @@ +'use strict'; + +const { mkdirSync, realpathSync } = require('fs'); +const { removeSync } = require('fs-extra'); +const path = require('path'); +const os = require('os'); +const crypto = require('crypto'); +const rmTmpDirIgnorableErrorCodes = require('./lib/private/rm-tmp-dir-ignorable-error-codes'); + +const systemTmpDir = realpathSync(os.tmpdir()); +const serverlessTmpDir = path.join(systemTmpDir, 'tmpdirs-serverless'); +try { + mkdirSync(serverlessTmpDir); +} catch (error) { + if (error.code !== 'EEXIST') throw error; +} + +module.exports = (function self() { + const processTmpDir = path.join(serverlessTmpDir, crypto.randomBytes(2).toString('hex')); + try { + mkdirSync(processTmpDir); + } catch (error) { + if (error.code !== 'EEXIST') throw error; + return self(); + } + return processTmpDir; +})(); + +process.on('exit', () => { + try { + removeSync(module.exports); + } catch (error) { + if (rmTmpDirIgnorableErrorCodes.has(error.code)) return; + throw error; + } +}); diff --git a/test/lib/provision-tmp-dir.js b/test/lib/provision-tmp-dir.js new file mode 100644 index 0000000000..d720c36714 --- /dev/null +++ b/test/lib/provision-tmp-dir.js @@ -0,0 +1,20 @@ +'use strict'; + +const path = require('path'); +const crypto = require('crypto'); +const { mkdir } = require('fs-extra'); +const processTmpDir = require('./process-tmp-dir'); + +module.exports = () => + new Promise((resolve) => { + const tmpDirName = path.join(processTmpDir, crypto.randomBytes(3).toString('hex')); + resolve( + mkdir(tmpDirName).then( + () => tmpDirName, + (error) => { + if (error.code !== 'EEXIST') throw error; + return module.exports(); // Name taken (rare edge case), retry + } + ) + ); + }); diff --git a/test/lib/resolve-aws-env.js b/test/lib/resolve-aws-env.js new file mode 100644 index 0000000000..e0616384ec --- /dev/null +++ b/test/lib/resolve-aws-env.js @@ -0,0 +1,15 @@ +'use strict'; + +const resolveEnv = require('./resolve-env'); + +module.exports = () => { + const env = resolveEnv({ + whitelist: ['SERVERLESS_ACCESS_KEY', 'SLS_AWS_REQUEST_MAX_RETRIES'], + }); + for (const envVarName of Object.keys(process.env)) { + if (envVarName.startsWith('AWS_') || envVarName.startsWith('SERVERLESS_PLATFORM_')) { + env[envVarName] = process.env[envVarName]; + } + } + return env; +}; diff --git a/test/lib/resolve-env.js b/test/lib/resolve-env.js new file mode 100644 index 0000000000..e26eab8ecb --- /dev/null +++ b/test/lib/resolve-env.js @@ -0,0 +1,26 @@ +'use strict'; + +const createEnv = require('process-utils/create-env'); + +module.exports = (options = {}) => { + if (!options) options = {}; + return createEnv({ + whitelist: [ + 'APPDATA', + 'HOME', + 'LOCAL_SERVERLESS_LINK_PATH', + 'LOG_LEVEL', + 'PATH', + 'SERVERLESS_BINARY_PATH', + 'SLS_SCHEMA_CACHE_BASE_DIR', + 'TEMP', + 'TMP', + 'TMPDIR', + 'USERPROFILE', + ].concat(options.whitelist || []), + variables: Object.assign( + { SLS_TRACKING_DISABLED: '1', SLS_DEPRECATION_NOTIFICATION_MODE: 'error' }, + options.variables || {} + ), + }); +}; diff --git a/test/lib/run-serverless.js b/test/lib/run-serverless.js new file mode 100644 index 0000000000..bc3bc2503c --- /dev/null +++ b/test/lib/run-serverless.js @@ -0,0 +1,381 @@ +'use strict'; + +const ensureString = require('type/string/ensure'); +const ensureIterable = require('type/iterable/ensure'); +const ensurePlainObject = require('type/plain-object/ensure'); +const ensurePlainFunction = require('type/plain-function/ensure'); +const _ = require('lodash'); +const cjsResolveSync = require('ncjsm/resolve/sync'); +const { writeJson } = require('fs-extra'); +const { entries, values } = require('lodash'); +const path = require('path'); +const os = require('os'); +const overrideEnv = require('process-utils/override-env'); +const overrideCwd = require('process-utils/override-cwd'); +const overrideArgv = require('process-utils/override-argv'); +const sinon = require('sinon'); +const resolveEnv = require('./resolve-env'); +const observeOutput = require('./observe-output'); +const disableServerlessStatsRequests = require('./disable-serverless-stats-requests'); +const provisionTmpDir = require('./provision-tmp-dir'); +const configureAwsRequestStub = require('./configure-aws-request-stub'); + +const resolveServerless = async (serverlessPath, modulesCacheStub, callback) => { + if (!modulesCacheStub) { + disableServerlessStatsRequests(serverlessPath); + return callback(require(serverlessPath)); + } + + const originalCache = Object.assign({}, require.cache); + for (const key of Object.keys(require.cache)) delete require.cache[key]; + disableServerlessStatsRequests(serverlessPath); + for (const [key, value] of entries(modulesCacheStub)) { + require.cache[path.isAbsolute(key) ? key : cjsResolveSync(serverlessPath, key).realPath] = { + exports: value, + }; + } + + const restore = () => { + for (const key of Object.keys(require.cache)) delete require.cache[key]; + Object.assign(require.cache, originalCache); + }; + try { + return await callback(require(serverlessPath)); + } finally { + restore(); + } +}; + +const resolveCwd = async ({ cwd, config }) => { + if (cwd) return cwd; + const tmpDirPath = await provisionTmpDir(); + await writeJson(path.join(tmpDirPath, 'serverless.json'), config); + return tmpDirPath; +}; + +module.exports = async ( + serverlessPath, + { + awsRequestStubMap, + command, + options, + config, + cwd, + env, + envWhitelist, + hooks, + lastLifecycleHookName, + lifecycleHookNamesBlacklist, + modulesCacheStub, + noService, + pluginPathsBlacklist, + shouldStubSpawn, + shouldUseLegacyVariablesResolver, + } +) => { + serverlessPath = path.resolve( + ensureString(serverlessPath, { + errorMessage: "Expected 'serverlessPath' to be a string. Received: %v", + }) + ); + try { + require.resolve(serverlessPath); + } catch (error) { + throw new TypeError( + `Provided 'serverlessPath' (${serverlessPath}) ` + + `doesn't point a working node module: ${error.message}` + ); + } + cwd = ensureString(cwd, { + isOptional: true, + errorMessage: 'Expected string value for `cwd` (current working directory), received %v', + }); + config = ensurePlainObject(config, { + isOptional: true, + errorMessage: 'Expected plain object value for `config`, received %v', + }); + if (!cwd && !config && !noService) { + throw new TypeError("Either 'cwd', 'config' or 'noService' option must be provided"); + } + if (cwd && config) { + throw new TypeError("Expected either 'cwd' or 'config' options, not both of them"); + } + if (noService) { + cwd = os.homedir(); + } else if (config) { + // By default expose configuration errors as crashes + if (!config.configValidationMode) config.configValidationMode = 'error'; + if (!config.frameworkVersion) config.frameworkVersion = '*'; + } + command = ensureString(command, { + errorMessage: 'Expected `command` to be a string, received %v', + }); + options = ensurePlainObject(options, { + default: {}, + errorMessage: 'Expected `options` to be a plain object, received %v', + }); + pluginPathsBlacklist = ensureIterable(pluginPathsBlacklist, { + default: [], + ensureItem: (pluginPath) => + require.resolve(path.resolve(serverlessPath, ensureString(pluginPath))), + errorMessage: + 'Expected `pluginPathsBlacklist` to be a valid plugin paths collection, received %v', + }); + lifecycleHookNamesBlacklist = ensureIterable(lifecycleHookNamesBlacklist, { + default: [], + ensureItem: ensureString, + errorMessage: 'Expected `lifecycleHookNamesBlacklist` to be a string collection, received %v', + }); + lastLifecycleHookName = ensureString(lastLifecycleHookName, { isOptional: true }); + hooks = ensurePlainObject(hooks, { + default: {}, + allowedKeys: ['after', 'before', 'beforeInstanceInit', 'beforeInstanceRun'], + ensurePropertyValue: ensurePlainFunction, + errorMessage: + 'Expected `hooks` to be a plain object with predefined supported hooks, received %v', + }); + env = ensurePlainObject(env, { + default: {}, + ensurePropertyValue: ensureString, + errorMessage: 'Expected `env` to be a plain object with string property values, received %v', + }); + envWhitelist = ensureIterable(envWhitelist, { + isOptional: true, + ensureItem: ensureString, + errorMessage: 'Expected `envWhitelist` to be a var names collection, received %v', + }); + awsRequestStubMap = ensurePlainObject(awsRequestStubMap, { isOptional: true }); + if (shouldStubSpawn) { + if (!modulesCacheStub) modulesCacheStub = {}; + modulesCacheStub['child-process-ext/spawn'] = sinon.stub().resolves({}); + } + const confirmedCwd = await resolveCwd({ cwd, config }); + + const resolveConfigurationPath = require(path.resolve( + serverlessPath, + 'lib/cli/resolve-configuration-path' + )); + const readConfiguration = require(path.resolve(serverlessPath, 'lib/configuration/read')); + const resolveVariables = require(path.resolve(serverlessPath, 'lib/configuration/variables')); + + return overrideEnv( + { + variables: Object.assign(resolveEnv(), { SLS_AWS_MONITORING_FREQUENCY: '1' }, env), + whitelist: envWhitelist, + }, + () => + overrideCwd(confirmedCwd, () => + resolveServerless(serverlessPath, modulesCacheStub, async (Serverless) => { + // Temporary patch to ensure resolveInput result matches the options + // (to be removed once we fully remove `resolveInput` dependency from `Serverless` class) + const resolveInput = require(path.resolve(serverlessPath, 'lib/cli/resolve-input')); + resolveInput.clear(); + overrideArgv( + { + args: [ + 'serverless', + ...command.split(' '), + ..._.flattenDeep( + Object.entries(options).map(([optionName, optionValue]) => { + if (optionValue === true) return `--${optionName}`; + if (optionValue === false) return `--no-${optionName}`; + if (optionValue === null) return null; + if (Array.isArray(optionValue)) { + return optionValue.map((optionItemValue) => [ + `--${optionName}`, + optionItemValue, + ]); + } + return [`--${optionName}`, optionValue]; + }) + ).filter(Boolean), + ], + }, + () => resolveInput(require(path.resolve(serverlessPath, 'lib/cli/commands-schema'))) + ); + + if (hooks.before) await hooks.before(Serverless, { cwd: confirmedCwd }); + // Intialize serverless instances in preconfigured environment + const configurationPath = await resolveConfigurationPath(); + const configuration = configurationPath + ? await readConfiguration(configurationPath) + : undefined; + + if (configuration && !shouldUseLegacyVariablesResolver) { + await resolveVariables({ + servicePath: path.dirname(configurationPath), + configuration, + options, + }); + } + + let serverless = new Serverless({ + configuration, + serviceDir: configurationPath && confirmedCwd, + configurationFilename: + configurationPath && configurationPath.slice(confirmedCwd.length + 1), + configurationPath, + isConfigurationResolved: !shouldUseLegacyVariablesResolver, + hasResolvedCommandsExternally: true, + commands: command ? command.split(' ') : [], + options, + }); + + if (serverless.triggeredDeprecations) { + serverless.triggeredDeprecations.clear(); + } + const pluginConstructorsBlacklist = pluginPathsBlacklist.map((pluginPath) => + require(pluginPath) + ); + try { + if (hooks.beforeInstanceInit) await hooks.beforeInstanceInit(serverless); + const output = await observeOutput(async () => { + await serverless.init(); + + if (serverless.invokedInstance) serverless = serverless.invokedInstance; + const { pluginManager } = serverless; + const blacklistedPlugins = pluginManager.plugins.filter((plugin) => + pluginConstructorsBlacklist.some((Plugin) => plugin instanceof Plugin) + ); + for (const [index, Plugin] of pluginConstructorsBlacklist.entries()) { + if (!blacklistedPlugins.some((plugin) => plugin instanceof Plugin)) { + throw new Error( + `Didn't resolve a plugin instance for ${pluginPathsBlacklist[index]}` + ); + } + } + + const { hooks: lifecycleHooks } = pluginManager; + const unconfirmedLifecycleHookNames = new Set(lifecycleHookNamesBlacklist); + for (const hookName of Object.keys(lifecycleHooks)) { + unconfirmedLifecycleHookNames.delete(hookName); + if (lifecycleHookNamesBlacklist.includes(hookName)) { + delete lifecycleHooks[hookName]; + continue; + } + + lifecycleHooks[hookName] = lifecycleHooks[hookName].filter( + (hookData) => + !blacklistedPlugins.some((blacklistedPlugin) => + values(blacklistedPlugin.hooks).includes(hookData.hook) + ) + ); + } + if (unconfirmedLifecycleHookNames.size) { + throw new Error( + `${Array.from(unconfirmedLifecycleHookNames).join( + ', ' + )} blacklisted lifecycle hook names were not recognized.` + ); + } + + if (lastLifecycleHookName) { + let hasLastHookFinalized = null; + if (pluginManager.getLifecycleEventsData) { + // Introduced in Serverless v2.60.0 + const { getLifecycleEventsData } = pluginManager; + pluginManager.getLifecycleEventsData = function (lifecycleCommand) { + if (hasLastHookFinalized) return { lifecycleEventsData: [], hooksLength: 0 }; + const result = getLifecycleEventsData.call(this, lifecycleCommand); + if (hasLastHookFinalized === false) return result; + + let newHooksLength = 0; + let lastHook; + let shouldOverride = false; + for (const [ + index, + { + hooksData: { before, at, after }, + lifecycleEventName, + }, + ] of result.lifecycleEventsData.entries()) { + newHooksLength += before.length; + if (before.length) lastHook = before[before.length - 1]; + if (lastLifecycleHookName === `before:${lifecycleEventName}`) { + at.length = 0; + after.length = 0; + } else { + newHooksLength += at.length; + if (at.length) lastHook = at[at.length - 1]; + if (lastLifecycleHookName === lifecycleEventName) { + after.length = 0; + } else { + newHooksLength += after.length; + if (after.length) lastHook = after[after.length - 1]; + if (lastLifecycleHookName !== `after:${lifecycleEventName}`) continue; + } + } + shouldOverride = Boolean(lastHook); + result.lifecycleEventsData.length = index + 1; + result.hooksLength = newHooksLength; + hasLastHookFinalized = !newHooksLength; + break; + } + if (shouldOverride) { + const hookFunction = lastHook.hook; + lastHook.hook = async function () { + try { + return await hookFunction.call(this); + } finally { + hasLastHookFinalized = true; + } + }; + } + return result; + }; + } else { + // TODO: Remove with next major release + const { getHooks } = pluginManager; + pluginManager.getHooks = function (events) { + if (hasLastHookFinalized) return []; + if (hasLastHookFinalized === false) { + return getHooks.call(this, events); + } + const lastEventIndex = events.indexOf(lastLifecycleHookName); + if (lastEventIndex === -1) return getHooks.call(this, events); + events = events.slice(0, lastEventIndex + 1); + const eventHooks = getHooks.call(this, events); + if (!eventHooks.length) { + hasLastHookFinalized = true; + return eventHooks; + } + hasLastHookFinalized = false; + const lastHook = eventHooks[eventHooks.length - 1]; + const hookFunction = lastHook.hook; + lastHook.hook = async function () { + try { + return await hookFunction.call(this); + } finally { + hasLastHookFinalized = true; + } + }; + return eventHooks; + }; + } + } + + if (awsRequestStubMap) { + configureAwsRequestStub(serverless.getProvider('aws'), awsRequestStubMap); + } + + if (hooks.beforeInstanceRun) await hooks.beforeInstanceRun(serverless); + // Run plugin manager hooks + await serverless.run(); + }); + if (hooks.after) await hooks.after(serverless); + const awsProvider = serverless.getProvider('aws'); + return { + serverless, + output, + cfTemplate: serverless.service.provider.compiledCloudFormationTemplate, + awsNaming: awsProvider && awsProvider.naming, + }; + } catch (error) { + throw Object.assign(error, { + serverless, + }); + } + }) + ) + ); +}; diff --git a/test/lib/setup-fixtures-engine.js b/test/lib/setup-fixtures-engine.js new file mode 100644 index 0000000000..d938259d1a --- /dev/null +++ b/test/lib/setup-fixtures-engine.js @@ -0,0 +1,149 @@ +'use strict'; + +const path = require('path'); +const ensureString = require('type/string/ensure'); +const ensurePlainObject = require('type/plain-object/ensure'); +const ensurePlainFunction = require('type/plain-function/ensure'); +const wait = require('timers-ext/promise/sleep'); +const spawn = require('child-process-ext/spawn'); +const fse = require('fs-extra'); +const memoizee = require('memoizee'); +const _ = require('lodash'); +const log = require('log').get('serverless:test'); +const { load: loadYaml, dump: saveYaml } = require('js-yaml'); +const cloudformationSchema = require('@serverless/utils/cloudformation-schema'); +const provisionTmpDir = require('./provision-tmp-dir'); + +const isFixtureConfigured = memoizee((fixturePath) => { + let stats; + try { + stats = fse.statSync(fixturePath); + } catch (error) { + if (error.code === 'ENOENT') return false; + throw error; + } + return Boolean(stats.isDirectory()); +}); + +const isFile = (filename) => + fse.lstat(filename).then( + (stats) => { + if (!stats.isFile()) return false; + return true; + }, + (error) => { + if (error.code === 'ENOENT') return false; + throw error; + } + ); + +const npmInstall = async (cwd, attempt = 0) => { + if (attempt) { + try { + await fse.remove(path.resolve(cwd, 'node_modules')); + } catch { + // ignore + } + } + + try { + await spawn('npm', ['install'], { cwd }); + } catch (error) { + if (attempt < 3) { + const { code, stdoutBuffer } = error; + if (code === 1) { + if (String(stdoutBuffer).includes('cb() never called!')) { + await wait(2000); + await npmInstall(cwd, ++attempt); + return; + } + } + } + throw error; + } +}; + +const setupFixture = memoizee( + async (fixturePath) => { + const [hasSetupScript, hasNpmDependencies] = await Promise.all([ + isFile(path.resolve(fixturePath, '_setup.js')), + isFile(path.resolve(fixturePath, 'package.json')), + ]); + if (!hasSetupScript && !hasNpmDependencies) return fixturePath; + const setupFixturePath = await provisionTmpDir(); + await fse.copy(fixturePath, setupFixturePath); + if (hasNpmDependencies) { + log.notice( + 'install dependencies for %s (at %s)', + path.basename(fixturePath), + setupFixturePath + ); + await npmInstall(setupFixturePath); + } + if (!hasSetupScript) return setupFixturePath; + log.notice('run setup for %s (at %s)', path.basename(fixturePath), setupFixturePath); + const setupScriptPath = path.resolve(setupFixturePath, '_setup.js'); + await ensurePlainFunction(require(setupScriptPath))(fixturePath); + await fse.unlink(setupScriptPath); + return setupFixturePath; + }, + { promise: true } +); + +const nameTimeBase = new Date(2020, 8, 7).getTime(); + +module.exports = memoizee((fixturesPath) => { + return { + setup: async (fixtureName, options = {}) => { + const baseFixturePath = path.join(fixturesPath, ensureString(fixtureName)); + if (!isFixtureConfigured(baseFixturePath)) { + throw new Error(`No fixture configured at ${fixtureName}`); + } + if (!options) options = {}; + + const [fixturePath, setupFixturePath] = await Promise.all([ + provisionTmpDir(), + setupFixture(baseFixturePath), + ]); + let configObject; + const [configContent] = await Promise.all([ + fse.readFile(path.join(setupFixturePath, 'serverless.yml')).catch((error) => { + if (error.code === 'ENOENT') return null; + throw error; + }), + fse.copy(setupFixturePath, fixturePath), + ]); + configObject = + configContent && + (() => { + try { + return loadYaml(configContent, { schema: cloudformationSchema }); + } catch (error) { + return null; + } + })(); + let isConfigUpdated = false; + if (_.get(configObject, 'service')) { + configObject.service = `test-${fixtureName}-${(Date.now() - nameTimeBase).toString(32)}`; + isConfigUpdated = true; + } + if (options.configExt) { + configObject = _.merge(configObject || {}, options.configExt); + isConfigUpdated = true; + } + if (isConfigUpdated) { + await fse.writeFile(path.join(fixturePath, 'serverless.yml'), saveYaml(configObject)); + } + log.info('setup %s fixture at %s', fixtureName, fixturePath); + return { + servicePath: fixturePath, + serviceConfig: configObject, + updateConfig: (configExt) => { + ensurePlainObject(configExt); + _.merge(configObject, configExt); + return fse.writeFile(path.join(fixturePath, 'serverless.yml'), saveYaml(configObject)); + }, + }; + }, + }; +}); diff --git a/test/lib/setup-run-serverless-fixtures-engine.js b/test/lib/setup-run-serverless-fixtures-engine.js new file mode 100644 index 0000000000..86bbead606 --- /dev/null +++ b/test/lib/setup-run-serverless-fixtures-engine.js @@ -0,0 +1,44 @@ +'use strict'; + +const ensureString = require('type/string/ensure'); +const ensurePlainFunction = require('type/plain-function/ensure'); +const ensurePlainObject = require('type/plain-object/ensure'); +const path = require('path'); +const runServerless = require('./run-serverless'); +const setupFixturesEngine = require('./setup-fixtures-engine'); + +module.exports = (setupOptions) => { + const fixturesDir = path.resolve(ensureString(ensurePlainObject(setupOptions).fixturesDir)); + const resolveServerlessDir = (() => { + if (ensurePlainFunction(setupOptions.resolveServerlessDir, { isOptional: true })) { + return setupOptions.resolveServerlessDir; + } + const serverlessDir = path.resolve(ensureString(setupOptions.serverlessDir)); + return () => serverlessDir; + })(); + + return async (options) => { + const runServerlessOptions = Object.assign({}, options); + delete runServerlessOptions.serverlessDir; + delete runServerlessOptions.fixture; + delete runServerlessOptions.configExt; + let fixtureData; + if (options.fixture) { + fixtureData = await setupFixturesEngine(fixturesDir).setup(options.fixture, { + configExt: options.configExt, + }); + runServerlessOptions.cwd = fixtureData.servicePath; + } + try { + const result = await runServerless( + options.serverlessDir || (await resolveServerlessDir()), + runServerlessOptions + ); + result.fixtureData = fixtureData; + return result; + } catch (error) { + error.fixtureData = fixtureData; + throw error; + } + }; +}; diff --git a/test/lib/setup/log.js b/test/lib/setup/log.js new file mode 100644 index 0000000000..882b7b177a --- /dev/null +++ b/test/lib/setup/log.js @@ -0,0 +1,66 @@ +'use strict'; + +if (!process.env.LOG_TIME) process.env.LOG_TIME = 'abs'; + +const log = require('log').get('mocha'); +const initializeLogWriter = require('log-node'); +const { runnerEmitter } = require('./patch'); + +const logWriter = initializeLogWriter(); + +const logSuiteTitle = (suite) => { + let message = '%s'; + const args = [suite.title]; + while (suite.parent) { + suite = suite.parent; + if (suite.title) { + message = `%s > ${message}`; + args.unshift(suite.title); + } + } + log.debug(message, ...args); +}; + +runnerEmitter.on('runner', (runner) => { + runner.on('suite', logSuiteTitle); + runner.on('test', logSuiteTitle); +}); + +if (process.env.LOG_LEVEL || process.env.LOG_DEBUG || process.env.DEBUG) return; + +// Flush all gathered logs (down to DEBUG level) on test failure + +const logEmitter = require('log/lib/emitter'); + +const logsBuffer = []; +const flushLogs = () => { + // Write, only if there are some non-mocha log events + if (logsBuffer.some((event) => event.logger.namespace !== 'mocha')) { + log.notice('flushing previously gathered logs...'); + logsBuffer.pop(); // Drop above log from logsBuffer + logsBuffer.forEach((event) => { + if (!event.message) logWriter.resolveMessage(event); + logWriter.writeMessage(event); + }); + } + logsBuffer.length = 0; // Empty array +}; + +logEmitter.on('log', (event) => { + logsBuffer.push(event); + if (!event.message) logWriter.resolveMessageTokens(event); +}); +runnerEmitter.on('runner', (runner) => { + runner.on('suite end', (suite) => { + if (!suite.parent || !suite.parent.root) return; + + logsBuffer.length = 0; // Empty array + }); + runner.on('fail', (ignore, error) => { + log.error('test fail %s', error && error.stack); + logsBuffer.pop(); // Drop above log from logsBuffer + flushLogs(); + }); +}); + +module.exports.flushLogs = flushLogs; diff --git a/test/lib/setup/mock-cwd.js b/test/lib/setup/mock-cwd.js new file mode 100644 index 0000000000..6c1a285443 --- /dev/null +++ b/test/lib/setup/mock-cwd.js @@ -0,0 +1,16 @@ +'use strict'; + +const os = require('os'); +const { runnerEmitter } = require('./patch'); + +const resetCwd = () => { + if (process.cwd() !== os.homedir()) process.chdir(os.homedir()); +}; + +runnerEmitter.on('runner', (runner) => { + resetCwd(); + runner.on('suite end', (suite) => { + if (!suite.parent || !suite.parent.root) return; // Apply just on top level suites + resetCwd(); + }); +}); diff --git a/test/lib/setup/mock-homedir.js b/test/lib/setup/mock-homedir.js new file mode 100644 index 0000000000..e0f0bd91b2 --- /dev/null +++ b/test/lib/setup/mock-homedir.js @@ -0,0 +1,47 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const crypto = require('crypto'); +const { emptyDirSync } = require('fs-extra'); +const processTmpDir = require('../process-tmp-dir'); +const { runnerEmitter } = require('./patch'); +const rmTmpDirIgnorableErrorCodes = require('../lib/private/rm-tmp-dir-ignorable-error-codes'); + +const createTmpHomedir = () => { + const tmpHomeDir = path.join(processTmpDir, crypto.randomBytes(3).toString('hex')); + try { + fs.mkdirSync(tmpHomeDir); + } catch (error) { + if (error.code === 'EEXIST') return createTmpHomedir(); + throw error; + } + return tmpHomeDir; +}; + +const tmpHomeDir = createTmpHomedir(); + +os.homedir = () => tmpHomeDir; +if (process.env.USERPROFILE) process.env.USERPROFILE = tmpHomeDir; +if (process.env.HOME) process.env.HOME = tmpHomeDir; + +runnerEmitter.on('runner', (runner) => { + runner.on('suite end', (suite) => { + if (!suite.parent || !suite.parent.root) return; + + // Cleanup temp homedir after each top level test run + try { + emptyDirSync(tmpHomeDir); + } catch (error) { + if (rmTmpDirIgnorableErrorCodes.has(error.code)) return; + // If some of the tests timed out, error could be caused by ongoing operation + // which still writse to temp dir, ignore it. + if (suite.tests.some((test) => test.timedOut)) return; + process.nextTick(() => { + // Crash in next tick, as otherwise Mocha goes bonkers + throw error; + }); + } + }); +}); diff --git a/test/lib/setup/patch.js b/test/lib/setup/patch.js new file mode 100644 index 0000000000..f48f327d8e --- /dev/null +++ b/test/lib/setup/patch.js @@ -0,0 +1,68 @@ +'use strict'; + +// Unhandled rejections are not exposed in Mocha, enforce it +// https://github.com/mochajs/mocha/issues/2640 +process.on('unhandledRejection', (err) => { + // Write to stderr + // (Mocha reports error to stdout and if we're muting it for test purposes it'll end silent) + process.stderr.write(`Unhandled rejection: ${err && err.stack}\n`); + throw err; +}); + +// By default report deprecations as errors +process.env.SLS_DEPRECATION_NOTIFICATION_MODE = 'error'; + +// Ensure no telemetry reporting in tests +process.env.SLS_TELEMETRY_DISABLED = '1'; + +const path = require('path'); +const EventEmitter = require('events'); + +const runnerEmitter = new EventEmitter(); + +const isObject = require('type/object/is'); +const resolveSync = require('ncjsm/resolve/sync'); + +// Ensure to resolve mocha from tested package context +const mochaBinPath = path.dirname(require.main.filename); +const Mocha = require(resolveSync(mochaBinPath, 'mocha/lib/mocha').realPath); +const { serialize } = require(resolveSync(mochaBinPath, 'mocha/lib/nodejs/serializer').realPath); + +const removeCyclicReferences = (object, parentObjects = new Set()) => { + const entries = Array.isArray(object) ? object.entries() : Object.entries(object); + parentObjects = new Set([...parentObjects, object]); + for (const [key, value] of entries) { + if (!isObject(value)) continue; + if (parentObjects.has(value)) delete object[key]; + else removeCyclicReferences(value, parentObjects); + } +}; + +const mochaRun = Mocha.prototype.run; +Mocha.prototype.run = function (fn, ...args) { + const runner = mochaRun.call( + this, + (result) => { + // Workaround https://github.com/mochajs/mocha/issues/4552 + const serialized = serialize(result); + const stringifiedResult = (() => { + try { + return JSON.stringify(serialized); + } catch (error) { + removeCyclicReferences(serialized); + return JSON.stringify(serialized); + } + })(); + return fn.call(this, JSON.parse(stringifiedResult)); + }, + ...args + ); + if (runner.constructor.name === 'Runner') runnerEmitter.emit('runner', runner); + // Ensure faster tests propagation + // It's to expose errors otherwise hidden by race conditions + // Reported to Mocha with: https://github.com/mochajs/mocha/issues/3920 + runner.constructor.immediately = process.nextTick; + return runner; +}; + +module.exports = { runnerEmitter }; diff --git a/test/lib/setup/restore-env.js b/test/lib/setup/restore-env.js new file mode 100644 index 0000000000..eab61bf82b --- /dev/null +++ b/test/lib/setup/restore-env.js @@ -0,0 +1,20 @@ +'use strict'; + +const { runnerEmitter } = require('./patch'); + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +const startEnv = Object.assign(Object.create(null), process.env); +runnerEmitter.on('runner', (runner) => + runner.on('suite end', (suite) => { + if (!suite.parent || !suite.parent.root) return; // Apply just on top level suites + for (const key of Object.keys(process.env)) { + if (!(key in startEnv)) delete process.env[key]; + } + for (const key of Object.keys(startEnv)) { + if (!hasOwnProperty.call(process.env, key) || process.env[key] !== startEnv[key]) { + process.env[key] = startEnv[key]; + } + } + }) +); diff --git a/test/lib/skip-on-disabled-symlinks-in-windows.js b/test/lib/skip-on-disabled-symlinks-in-windows.js new file mode 100644 index 0000000000..9ac1505d8c --- /dev/null +++ b/test/lib/skip-on-disabled-symlinks-in-windows.js @@ -0,0 +1,8 @@ +'use strict'; + +const skipWithNotice = require('./skip-with-notice'); + +module.exports = (error, context, afterCallback) => { + if (error.code !== 'EPERM' || process.platform !== 'win32') return; + skipWithNotice(context, 'Missing admin rights to create symlinks', afterCallback); +}; diff --git a/test/lib/skip-with-notice.js b/test/lib/skip-with-notice.js new file mode 100644 index 0000000000..d65d4c0553 --- /dev/null +++ b/test/lib/skip-with-notice.js @@ -0,0 +1,53 @@ +'use strict'; + +const chalk = require('chalk'); +const { runnerEmitter } = require('./setup/patch'); + +const skippedWithNotice = []; + +module.exports = (context, reason, afterCallback) => { + if (!context || typeof context.skip !== 'function') { + throw new TypeError('Passed context is not a valid mocha suite'); + } + if (process.env.CI) return; // Do not tolerate skips in CI environment + + skippedWithNotice.push({ context, reason }); + process.stdout.write(chalk.yellow(`\n Skipped due to: ${chalk.red(reason)}\n\n`)); + + if (afterCallback) { + try { + // Ensure teardown is called + // (Mocha fails to do it -> https://github.com/mochajs/mocha/issues/3740) + afterCallback(); + } catch (error) { + process.stdout.write(chalk.error(`after callback crashed with: ${error.stack}\n`)); + } + } + context.skip(); +}; + +runnerEmitter.on('runner', (runner) => + runner.on('end', () => { + // Output eventual skip notices + if (!skippedWithNotice.length) return; + + const resolveTestName = (test) => { + const names = [test.title]; + let parent = test.parent; + while (parent) { + if (parent.title) names.push(parent.title); + parent = parent.parent; + } + return `${chalk.cyan(names.reverse().join(': '))} (in: ${chalk.grey( + test.file.slice(process.cwd().length + 1) + )})`; + }; + + process.stdout.write( + ' Notice: Some tests were skipped due to following environment issues:' + + `\n\n - ${skippedWithNotice + .map((meta) => `${resolveTestName(meta.context.test)}\n\n ${chalk.red(meta.reason)}\n`) + .join('\n - ')}\n\n` + ); + }) +); diff --git a/test/mocha-patch.js b/test/mocha-patch.js index 992ee2cb61..1d9111a330 100644 --- a/test/mocha-patch.js +++ b/test/mocha-patch.js @@ -56,7 +56,7 @@ BbPromise.config({ longStackTraces: true, }); -const { runnerEmitter } = require('@serverless/test/setup/patch'); +const { runnerEmitter } = require('./lib/setup/patch'); runnerEmitter.on('runner', (runner) => { runner.on('suite end', (suite) => { diff --git a/test/unit/lib/cli/handle-error.test.js b/test/unit/lib/cli/handle-error.test.js index 78ccd0d48b..4a4b40e12b 100644 --- a/test/unit/lib/cli/handle-error.test.js +++ b/test/unit/lib/cli/handle-error.test.js @@ -2,7 +2,7 @@ const chai = require('chai'); -const observeOutput = require('@serverless/test/observe-output'); +const observeOutput = require('../../../lib/observe-output'); const handleError = require('../../../../lib/cli/handle-error'); const isStandaloneExecutable = require('../../../../lib/utils/is-standalone-executable'); const ServerlessError = require('../../../../lib/serverless-error'); diff --git a/test/unit/lib/cli/render-help/command.test.js b/test/unit/lib/cli/render-help/command.test.js index ae46516328..5992fc3552 100644 --- a/test/unit/lib/cli/render-help/command.test.js +++ b/test/unit/lib/cli/render-help/command.test.js @@ -3,7 +3,7 @@ const { expect } = require('chai'); const renderCommandHelp = require('../../../../../lib/cli/render-help/command'); const commandsSchema = require('../../../../../lib/cli/commands-schema'); -const observeOutput = require('@serverless/test/observe-output'); +const observeOutput = require('../../../../lib/observe-output'); describe('test/unit/lib/cli/render-help/command.test.js', () => { it('should show help', async () => { diff --git a/test/unit/lib/cli/render-help/general.test.js b/test/unit/lib/cli/render-help/general.test.js index 240a1d6332..f621f7a44a 100644 --- a/test/unit/lib/cli/render-help/general.test.js +++ b/test/unit/lib/cli/render-help/general.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const renderGeneralHelp = require('../../../../../lib/cli/render-help/general'); -const observeOutput = require('@serverless/test/observe-output'); +const observeOutput = require('../../../../lib/observe-output'); describe('test/unit/lib/cli/render-help/general.test.js', () => { it('should show help', async () => { diff --git a/test/unit/lib/cli/render-help/index.test.js b/test/unit/lib/cli/render-help/index.test.js index 340af068da..7d6a9b9617 100644 --- a/test/unit/lib/cli/render-help/index.test.js +++ b/test/unit/lib/cli/render-help/index.test.js @@ -4,7 +4,7 @@ const { expect } = require('chai'); const overrideArgv = require('process-utils/override-argv'); const resolveInput = require('../../../../../lib/cli/resolve-input'); const renderHelp = require('../../../../../lib/cli/render-help'); -const observeOutput = require('@serverless/test/observe-output'); +const observeOutput = require('../../../../lib/observe-output'); describe('test/unit/lib/cli/render-help/index.test.js', () => { it('should show general help on main command', async () => { diff --git a/test/unit/lib/cli/render-help/options.test.js b/test/unit/lib/cli/render-help/options.test.js index 901440504f..20a8384d33 100644 --- a/test/unit/lib/cli/render-help/options.test.js +++ b/test/unit/lib/cli/render-help/options.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const renderOptionsHelp = require('../../../../../lib/cli/render-help/options'); -const observeOutput = require('@serverless/test/observe-output'); +const observeOutput = require('../../../../lib/observe-output'); describe('test/unit/lib/cli/render-help/options.test.js', () => { it('should list options', async () => { diff --git a/test/unit/lib/cli/render-version.test.js b/test/unit/lib/cli/render-version.test.js index 38c6012e38..4eae38b9a0 100644 --- a/test/unit/lib/cli/render-version.test.js +++ b/test/unit/lib/cli/render-version.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const listVersion = require('../../../../lib/cli/render-version'); -const observeOutput = require('@serverless/test/observe-output'); +const observeOutput = require('../../../lib/observe-output'); describe('test/unit/lib/cli/list-version.test.js', () => { it('should log version', async () => { diff --git a/test/unit/lib/plugins/aws/invoke-local/index.test.js b/test/unit/lib/plugins/aws/invoke-local/index.test.js index c603d74a6f..c663ad5b08 100644 --- a/test/unit/lib/plugins/aws/invoke-local/index.test.js +++ b/test/unit/lib/plugins/aws/invoke-local/index.test.js @@ -12,7 +12,7 @@ const AwsProvider = require('../../../../../../lib/plugins/aws/provider'); const Serverless = require('../../../../../../lib/serverless'); const CLI = require('../../../../../../lib/classes/cli'); const { getTmpDirPath } = require('../../../../../utils/fs'); -const skipWithNotice = require('@serverless/test/skip-with-notice'); +const skipWithNotice = require('../../../../../lib/skip-with-notice'); const runServerless = require('../../../../../utils/run-serverless'); const spawnExt = require('child-process-ext/spawn'); diff --git a/test/unit/lib/plugins/plugin/lib/utils.test.js b/test/unit/lib/plugins/plugin/lib/utils.test.js index 435b570865..c1a03edd00 100644 --- a/test/unit/lib/plugins/plugin/lib/utils.test.js +++ b/test/unit/lib/plugins/plugin/lib/utils.test.js @@ -6,7 +6,7 @@ const proxyquire = require('proxyquire'); const PluginList = require('../../../../../../lib/plugins/plugin/list'); const Serverless = require('../../../../../../lib/serverless'); const CLI = require('../../../../../../lib/classes/cli'); -const observeOutput = require('@serverless/test/observe-output'); +const observeOutput = require('../../../../../lib/observe-output'); chai.use(require('chai-as-promised')); diff --git a/test/unit/lib/utils/fs/copy-dir-contents-sync.test.js b/test/unit/lib/utils/fs/copy-dir-contents-sync.test.js index 86676cd9e2..4e2ba0a088 100644 --- a/test/unit/lib/utils/fs/copy-dir-contents-sync.test.js +++ b/test/unit/lib/utils/fs/copy-dir-contents-sync.test.js @@ -7,7 +7,7 @@ const path = require('path'); const copyDirContentsSync = require('../../../../../lib/utils/fs/copy-dir-contents-sync'); const fileExistsSync = require('../../../../../lib/utils/fs/file-exists-sync'); const writeFileSync = require('../../../../../lib/utils/fs/write-file-sync'); -const skipOnDisabledSymlinksInWindows = require('@serverless/test/skip-on-disabled-symlinks-in-windows'); +const skipOnDisabledSymlinksInWindows = require('../../../../lib/skip-on-disabled-symlinks-in-windows'); describe('#copyDirContentsSync()', () => { const afterCallback = () => { diff --git a/test/unit/lib/utils/fs/file-exists-sync.test.js b/test/unit/lib/utils/fs/file-exists-sync.test.js index 7a0c554a9c..2838f518ae 100644 --- a/test/unit/lib/utils/fs/file-exists-sync.test.js +++ b/test/unit/lib/utils/fs/file-exists-sync.test.js @@ -3,7 +3,7 @@ const path = require('path'); const expect = require('chai').expect; const fse = require('fs-extra'); -const skipOnDisabledSymlinksInWindows = require('@serverless/test/skip-on-disabled-symlinks-in-windows'); +const skipOnDisabledSymlinksInWindows = require('../../../../lib/skip-on-disabled-symlinks-in-windows'); const fileExistsSync = require('../../../../../lib/utils/fs/file-exists-sync'); describe('#fileExistsSync()', () => { diff --git a/test/unit/lib/utils/fs/safe-move-file.test.js b/test/unit/lib/utils/fs/safe-move-file.test.js index 0f498870cc..34b7e903a8 100644 --- a/test/unit/lib/utils/fs/safe-move-file.test.js +++ b/test/unit/lib/utils/fs/safe-move-file.test.js @@ -3,7 +3,7 @@ const fse = require('fs-extra'); const sinon = require('sinon'); const chai = require('chai'); -const provisionTempDir = require('@serverless/test/provision-tmp-dir'); +const provisionTempDir = require('../../../../lib/provision-tmp-dir'); const { join } = require('path'); const { expect } = require('chai'); const fsp = require('fs').promises; diff --git a/test/unit/lib/utils/fs/walk-dir-sync.test.js b/test/unit/lib/utils/fs/walk-dir-sync.test.js index 8bc9ac52d4..5e1433ecb8 100644 --- a/test/unit/lib/utils/fs/walk-dir-sync.test.js +++ b/test/unit/lib/utils/fs/walk-dir-sync.test.js @@ -6,7 +6,7 @@ const writeFileSync = require('../../../../../lib/utils/fs/write-file-sync'); const walkDirSync = require('../../../../../lib/utils/fs/walk-dir-sync'); const { expect } = require('chai'); const { getTmpDirPath } = require('../../../../utils/fs'); -const skipOnDisabledSymlinksInWindows = require('@serverless/test/skip-on-disabled-symlinks-in-windows'); +const skipOnDisabledSymlinksInWindows = require('../../../../lib/skip-on-disabled-symlinks-in-windows'); describe('#walkDirSync()', () => { it('should return an array with corresponding paths to the found files', () => { diff --git a/test/utils/api-gateway.js b/test/utils/api-gateway.js index 8a0f889053..10091671c9 100644 --- a/test/utils/api-gateway.js +++ b/test/utils/api-gateway.js @@ -1,7 +1,7 @@ 'use strict'; const _ = require('lodash'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getAPIGatewayClient = () => { diff --git a/test/utils/cloudformation.js b/test/utils/cloudformation.js index fc2bc02b92..691ffa7696 100644 --- a/test/utils/cloudformation.js +++ b/test/utils/cloudformation.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getCloudFormationClient = () => { diff --git a/test/utils/cognito.js b/test/utils/cognito.js index 9918a0bfcc..9c130e3ed8 100644 --- a/test/utils/cognito.js +++ b/test/utils/cognito.js @@ -1,7 +1,7 @@ 'use strict'; const awsLog = require('log').get('aws'); -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getCognitoClient = () => { diff --git a/test/utils/dynamodb.js b/test/utils/dynamodb.js index 39c85f5926..74a7e30c70 100644 --- a/test/utils/dynamodb.js +++ b/test/utils/dynamodb.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getDynamoDBClient = () => { diff --git a/test/utils/event-bridge.js b/test/utils/event-bridge.js index 5c2d4e0341..8081878153 100644 --- a/test/utils/event-bridge.js +++ b/test/utils/event-bridge.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getEventBridgeClient = () => { diff --git a/test/utils/fs.js b/test/utils/fs.js index 50f43cce24..f32fd9e3c1 100644 --- a/test/utils/fs.js +++ b/test/utils/fs.js @@ -7,7 +7,7 @@ const crypto = require('crypto'); const yaml = require('js-yaml'); const JSZip = require('jszip'); -const tmpDirCommonPath = require('@serverless/test/process-tmp-dir'); +const tmpDirCommonPath = require('../lib/process-tmp-dir'); function getTmpDirPath() { return path.join(tmpDirCommonPath, crypto.randomBytes(8).toString('hex')); diff --git a/test/utils/integration.js b/test/utils/integration.js index 4a657acdea..ede8e18e97 100644 --- a/test/utils/integration.js +++ b/test/utils/integration.js @@ -8,7 +8,7 @@ const spawn = require('child-process-ext/spawn'); const nodeFetch = require('node-fetch'); const log = require('log').get('serverless:test'); const logFetch = require('log').get('fetch'); -const resolveAwsEnv = require('@serverless/test/resolve-aws-env'); +const resolveAwsEnv = require('../lib/resolve-aws-env'); const { load: loadYaml } = require('js-yaml'); const serverlessExec = require('../serverless-binary'); diff --git a/test/utils/iot.js b/test/utils/iot.js index 3a128075fc..b3aacea050 100644 --- a/test/utils/iot.js +++ b/test/utils/iot.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getIoTClients = () => { diff --git a/test/utils/kinesis.js b/test/utils/kinesis.js index e11dca4fe3..c1a70958fa 100644 --- a/test/utils/kinesis.js +++ b/test/utils/kinesis.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getKinesisClient = () => { diff --git a/test/utils/misc.js b/test/utils/misc.js index f1d92e475b..8f173c56b5 100644 --- a/test/utils/misc.js +++ b/test/utils/misc.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); const wait = require('timers-ext/promise/sleep'); // Support for both AWS SDK v2 and v3 diff --git a/test/utils/run-serverless.js b/test/utils/run-serverless.js index 06aabd8a45..1d2be05566 100644 --- a/test/utils/run-serverless.js +++ b/test/utils/run-serverless.js @@ -2,7 +2,7 @@ const path = require('path'); -module.exports = require('@serverless/test/setup-run-serverless-fixtures-engine')({ +module.exports = require('../lib/setup-run-serverless-fixtures-engine')({ fixturesDir: path.resolve(__dirname, '../fixtures/programmatic'), serverlessDir: path.resolve(__dirname, '../../'), }); diff --git a/test/utils/s3.js b/test/utils/s3.js index 7fd95bc4aa..b03d36d4d1 100644 --- a/test/utils/s3.js +++ b/test/utils/s3.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getS3Client = () => { diff --git a/test/utils/sns.js b/test/utils/sns.js index a10bfb45b0..c7b4d37346 100644 --- a/test/utils/sns.js +++ b/test/utils/sns.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getSNSClient = () => { diff --git a/test/utils/sqs.js b/test/utils/sqs.js index 7c54f8ed26..ecf4f61237 100644 --- a/test/utils/sqs.js +++ b/test/utils/sqs.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getSQSClient = () => { diff --git a/test/utils/websocket.js b/test/utils/websocket.js index 260042e033..d87e949555 100644 --- a/test/utils/websocket.js +++ b/test/utils/websocket.js @@ -1,6 +1,6 @@ 'use strict'; -const awsRequest = require('@serverless/test/aws-request'); +const awsRequest = require('../lib/aws-request'); // Support for both AWS SDK v2 and v3 const getApiGatewayV2Client = () => {