From 0619ca12503ba9c0b02ddf62bf8c8ad16dab221f Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 22 Apr 2026 15:13:36 +0100 Subject: [PATCH 1/2] Upgrade to mocha 12 --- .github/workflows/publish.yml | 12 +- .github/workflows/tests.yml | 34 +++--- README.md | 2 +- bin/serverless-compose | 9 +- package.json | 16 +-- src/ComponentsService.js | 6 +- src/Context.js | 4 +- src/cli/is-supported-node-version.js | 14 ++- src/configuration/resolve-path.js | 7 +- src/index.js | 13 ++- src/state/LocalStateStorage.js | 4 +- .../serverless-utils/log-reporters/node.js | 107 ++++++++++-------- test/lib/setup/log.js | 63 ----------- test/lib/setup/mock-cwd.js | 16 --- test/lib/setup/mock-homedir.js | 43 ------- test/lib/setup/patch.js | 25 ---- test/lib/setup/restore-env.js | 20 ---- test/mocha/bootstrap.cjs | 10 ++ test/mocha/root-hooks.cjs | 23 ++++ test/mocha/runtime-sandbox.cjs | 60 ++++++++++ test/mocha/unit.cjs | 9 ++ test/unit/bin/serverless-compose.test.js | 36 +++--- test/unit/components/framework/index.test.js | 5 +- test/unit/src/Context.test.js | 17 +-- .../src/cli/is-supported-node-version.test.js | 20 +++- test/unit/src/components-service.test.js | 2 - test/unit/src/configuration/read.test.js | 1 - test/unit/src/state/S3StateStorage.test.js | 3 - .../get-s3-state-storage-from-config.test.js | 2 - .../state/utils/get-state-bucket-name.test.js | 3 - .../utils/get-state-bucket-region.test.js | 3 - .../src/utils/aws/default-provider.test.js | 2 - .../aws/from-node-provider-chain.test.js | 2 - .../src/utils/aws/get-client-config.test.js | 2 - .../utils/aws/get-credential-provider.test.js | 2 - .../src/utils/aws/remote-provider.test.js | 3 - .../log-reporters/node.test.js | 94 +++++++-------- .../node/progress-reporter.test.js | 2 - 38 files changed, 296 insertions(+), 400 deletions(-) delete mode 100644 test/lib/setup/log.js delete mode 100644 test/lib/setup/mock-cwd.js delete mode 100644 test/lib/setup/mock-homedir.js delete mode 100644 test/lib/setup/patch.js delete mode 100644 test/lib/setup/restore-env.js create mode 100644 test/mocha/bootstrap.cjs create mode 100644 test/mocha/root-hooks.cjs create mode 100644 test/mocha/runtime-sandbox.cjs create mode 100644 test/mocha/unit.cjs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d7c8712..9c7faf1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,23 +14,23 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Retrieve dependencies from cache - uses: actions/cache@v4 + uses: actions/cache@v5 id: cacheNpm with: path: ~/.npm - key: npm-v22-${{ runner.os }}-refs/heads/main-${{ hashFiles('package.json') }} + key: npm-v22-${{ runner.os }}-refs/heads/main-${{ hashFiles('package-lock.json') }} - name: Install Node.js and npm - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22.x registry-url: https://registry.npmjs.org - name: Install dependencies - run: npm install + run: npm ci # Store the name of the release # See https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions @@ -70,7 +70,7 @@ jobs: name: npm-package - name: Install Node.js and npm - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22.x registry-url: https://registry.npmjs.org diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a24c1b..3c05ceb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 30 @@ -28,21 +28,21 @@ jobs: - name: Retrieve dependencies from cache id: cacheNpm - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm - key: npm-v22-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} + key: npm-v22-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package-lock.json') }} restore-keys: | npm-v22-${{ runner.os }}-${{ github.ref }}- npm-v22-${{ runner.os }}-refs/heads/main- - name: Install Node.js and npm - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22.x - name: Install dependencies - run: npm install + run: npm ci - name: Validate formatting run: npm run prettier-check:updated @@ -64,25 +64,25 @@ jobs: runs-on: windows-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Retrieve dependencies from cache id: cacheNpm - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm - key: npm-v20-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} + key: npm-v22-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package-lock.json') }} restore-keys: | - npm-v20-${{ runner.os }}-${{ github.ref }}- - npm-v20-${{ runner.os }}-refs/heads/main- + npm-v22-${{ runner.os }}-${{ github.ref }}- + npm-v22-${{ runner.os }}-refs/heads/main- - name: Install Node.js and npm - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22.x - name: Install dependencies - run: npm install + run: npm ci - name: Unit tests run: npm test @@ -92,25 +92,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Retrieve dependencies from cache id: cacheNpm - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm - key: npm-v20-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} + key: npm-v20-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package-lock.json') }} restore-keys: | npm-v20-${{ runner.os }}-${{ github.ref }}- npm-v20-${{ runner.os }}-refs/heads/main- - name: Install Node.js and npm - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 20.x - name: Install dependencies - run: npm install + run: npm ci - name: Unit tests run: npm test diff --git a/README.md b/README.md index ec87159..52bf384 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ This repository contains the code for Serverless Framework Compose. -Compose requires Node.js 20 or later. +Requires Node.js `^20.19.0 || >=22.12.0`. There is no standalone binary version, the package is only available via NPM. diff --git a/bin/serverless-compose b/bin/serverless-compose index 077e047..582d87b 100755 --- a/bin/serverless-compose +++ b/bin/serverless-compose @@ -7,16 +7,13 @@ const isMainModule = !EvalError.$composeCommandStartTime; if (isMainModule) EvalError.$composeCommandStartTime = process.hrtime(); const isSupportedNodeVersion = require('../src/cli/is-supported-node-version'); -const minimumSupportedVersionMajor = 20; -const minimumSupportedVersionMinor = 0; if (!isSupportedNodeVersion(process.version)) { const composeVersion = require('../package.json').version; process.stderr.write( `Error: Serverless Framework Compose v${composeVersion} does not support ` + - `Node.js ${process.version}. Please upgrade Node.js to the latest ` + - `LTS release. Minimum supported version: ` + - `v${minimumSupportedVersionMajor}.${minimumSupportedVersionMinor}.0.\n` + `Node.js ${process.version}. Please use a supported release. ` + + `Supported versions: ${isSupportedNodeVersion.supportedRange}.\n` ); process.exit(1); } @@ -30,4 +27,4 @@ const crashAsync = (error) => { const { runComponents } = require('../src'); // Compose is npm-only, so there is no standalone or local-fallback bootstrap path here. -runComponents().catch(crashAsync); +runComponents(process.argv.slice(2)).catch(crashAsync); diff --git a/package.json b/package.json index 2acf709..6448011 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "prettify": "prettier --write --ignore-path .gitignore \"**/*.{css,html,js,json,md,yaml,yml}\"", "prettier-check:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml --base=main -- prettier -c", "lint:updated": "pipe-git-updated --ext=js --base=main -- eslint", - "test": "mocha \"test/**/*.test.js\"" + "test": "mocha --config test/mocha/unit.cjs" }, "license": "MIT", "dependencies": { @@ -73,7 +73,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-n": "^17.24.0", "git-list-updated": "^1.2.1", - "mocha": "^11.7.5", + "mocha": "12.0.0-beta-9.2", "prettier": "^2.8.8", "proxyquire": "^2.1.3", "sinon": "^13.0.2", @@ -343,18 +343,8 @@ } ] }, - "mocha": { - "require": [ - "./test/lib/setup/patch", - "./test/lib/setup/log", - "./test/lib/setup/mock-homedir", - "./test/lib/setup/mock-cwd", - "./test/lib/setup/restore-env" - ], - "timeout": 10000 - }, "engines": { - "node": ">=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "directories": { "test": "test" diff --git a/src/ComponentsService.js b/src/ComponentsService.js index 4f6d44f..d79d6f2 100644 --- a/src/ComponentsService.js +++ b/src/ComponentsService.js @@ -79,7 +79,7 @@ const validateGraph = (graph) => { } }; -const getAllComponents = async (obj = {}) => { +const getAllComponents = async (obj = {}, root = process.cwd()) => { const allComponents = {}; for (const [key, val] of Object.entries(obj.services)) { @@ -90,7 +90,7 @@ const getAllComponents = async (obj = {}) => { // Local component (starts with '.') if (val.component[0] === '.') { - const localComponentPath = resolve(process.cwd(), val.component); + const localComponentPath = resolve(root, val.component); if (!(await utils.fileExists(localComponentPath))) { throw new ServerlessError( `The component "${val.component}" (used by service "${key}") is invalid: file not found`, @@ -237,7 +237,7 @@ class ComponentsService { } async init() { - const allComponents = await getAllComponents(this.configuration); + const allComponents = await getAllComponents(this.configuration, this.context.root); await validateComponents(allComponents); this.allComponents = setDependencies(allComponents); diff --git a/src/Context.js b/src/Context.js index 4f8ec43..701f9d3 100644 --- a/src/Context.js +++ b/src/Context.js @@ -16,7 +16,7 @@ const ServerlessError = require('./serverless-error'); class Context { constructor(config) { this.version = packageJson.version; - this.root = path.resolve(config.root) || process.cwd(); + this.root = path.resolve(config.root || process.cwd()); this.output = new Output(config.verbose || false, config.disableIO); /** @type {string} */ this.stage = config.stage; @@ -42,7 +42,7 @@ class Context { async setupStateStorage() { if (!this.configuration.state) { - this.stateStorage = new LocalStateStorage(this.stage); + this.stateStorage = new LocalStateStorage(this.root, this.stage); return; } diff --git a/src/cli/is-supported-node-version.js b/src/cli/is-supported-node-version.js index f3e08a6..a9b5ef4 100644 --- a/src/cli/is-supported-node-version.js +++ b/src/cli/is-supported-node-version.js @@ -1,8 +1,14 @@ 'use strict'; -module.exports = (version) => { - const major = Number(version.split('.')[0].slice(1)); - const minor = Number(version.split('.')[1]); +const semver = require('semver'); - return major > 20 || (major === 20 && minor >= 0); +const supportedRange = '^20.19.0 || >=22.12.0'; + +const isSupportedNodeVersion = (version) => { + const normalizedVersion = semver.coerce(version); + + return Boolean(normalizedVersion && semver.satisfies(normalizedVersion.version, supportedRange)); }; + +module.exports = isSupportedNodeVersion; +module.exports.supportedRange = supportedRange; diff --git a/src/configuration/resolve-path.js b/src/configuration/resolve-path.js index ed98896..dfe16ae 100644 --- a/src/configuration/resolve-path.js +++ b/src/configuration/resolve-path.js @@ -6,12 +6,9 @@ const ServerlessError = require('../serverless-error'); const supportedExtensions = new Set(['yml', 'yaml', 'json', 'js', 'ts']); -module.exports = async () => { +module.exports = async (cwd = process.cwd()) => { for (const extension of supportedExtensions) { - const eventualServiceConfigPath = path.resolve( - process.cwd(), - `serverless-compose.${extension}` - ); + const eventualServiceConfigPath = path.resolve(cwd, `serverless-compose.${extension}`); if (await fileExists(eventualServiceConfigPath)) return eventualServiceConfigPath; } // As the default will be `serverless-compose.yml`, let's provide users with a more actionable error message, diff --git a/src/index.js b/src/index.js index 96b8278..694c65b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,6 @@ 'use strict'; -// Setup log writing -require('./utils/serverless-utils/log-reporters/node'); - -const args = require('minimist')(process.argv.slice(2)); +const minimist = require('minimist'); const renderHelp = require('./render-help'); const Context = require('./Context'); const ComponentsService = require('./ComponentsService'); @@ -14,6 +11,7 @@ const resolveConfigurationPath = require('./configuration/resolve-path'); const readConfiguration = require('./configuration/read'); const { validateConfiguration } = require('./configuration/validate'); const validateOptions = require('./validate-options'); +const initializeNodeLogging = require('./utils/serverless-utils/log-reporters/node'); let options; let method; @@ -39,7 +37,10 @@ require('signal-exit/signals').forEach((signal) => { }); }); -const runComponents = async () => { +const runComponents = async (argv = process.argv.slice(2)) => { + const args = minimist(argv); + initializeNodeLogging({ argv, env: process.env, stdin: process.stdin, stdout: process.stdout }); + if (args.help || args._[0] === 'help') { await renderHelp(); return; @@ -63,7 +64,7 @@ const runComponents = async () => { } delete options._; // remove the method name if any - const configurationPath = await resolveConfigurationPath(); + const configurationPath = await resolveConfigurationPath(process.cwd()); const configuration = await readConfiguration(configurationPath); validateConfiguration(configuration, configurationPath); diff --git a/src/state/LocalStateStorage.js b/src/state/LocalStateStorage.js index 00ba8a2..6800f35 100644 --- a/src/state/LocalStateStorage.js +++ b/src/state/LocalStateStorage.js @@ -6,9 +6,9 @@ const path = require('path'); const fsp = require('fs').promises; class LocalStateStorage extends BaseStateStorage { - constructor(stage) { + constructor(root, stage) { super(); - this.stateRoot = path.join(process.cwd(), '.serverless'); + this.stateRoot = path.join(root, '.serverless'); this.stage = stage; } diff --git a/src/utils/serverless-utils/log-reporters/node.js b/src/utils/serverless-utils/log-reporters/node.js index e58e5c3..22c6523 100644 --- a/src/utils/serverless-utils/log-reporters/node.js +++ b/src/utils/serverless-utils/log-reporters/node.js @@ -1,50 +1,61 @@ 'use strict'; -const uniGlobal = require('uni-global')('serverless/serverless/202110'); - -if (uniGlobal.logLevelIndex != null) { - return; -} - -if (process.env.SLS_LOG_LEVEL !== 'debug' && process.argv.includes('--verbose')) { - process.env.SLS_LOG_LEVEL = 'info'; -} - -process.argv.some((flag, index) => { - const namespace = (() => { - if (flag === '--debug') return process.argv[index + 1]; - if (flag.startsWith('--debug=')) return flag.slice('--debug='.length); - return null; - })(); - if (!namespace) return false; - if (namespace === '*') process.env.SLS_LOG_LEVEL = 'debug'; - else process.env.SLS_LOG_DEBUG = namespace; - return true; -}); - -const logReporter = require('../lib/log-reporters/node/log-reporter'); -const { emitter: outputEmitter } = require('../lib/log/get-output-reporter'); -const joinTextTokens = require('../lib/log/join-text-tokens'); -const logLevels = require('log/levels'); - -const logLevelIndex = logLevels.includes(process.env.SLS_LOG_LEVEL) - ? logLevels.indexOf(process.env.SLS_LOG_LEVEL) - : logLevels.indexOf('notice'); - -const isInteractive = - (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) || - process.env.SLS_INTERACTIVE_SETUP_ENABLE; - -require('../lib/log-reporters/node/style'); - -logReporter({ logLevelIndex, debugNamespaces: process.env.SLS_LOG_DEBUG }); -uniGlobal.logLevelIndex = logLevelIndex; - -outputEmitter.on('write', ({ mode, textTokens }) => { - if (mode === 'text') process.stdout.write(joinTextTokens(textTokens)); -}); - -uniGlobal.logIsInteractive = isInteractive; -if (isInteractive) { - require('../lib/log-reporters/node/progress-reporter')({ logLevelIndex }); -} +module.exports = ({ + argv = process.argv.slice(2), + env = process.env, + stdin = process.stdin, + stdout = process.stdout, +} = {}) => { + const uniGlobal = require('uni-global')('serverless/serverless/202110'); + + if (uniGlobal.logLevelIndex != null) { + return { + logLevelIndex: uniGlobal.logLevelIndex, + isInteractive: uniGlobal.logIsInteractive, + }; + } + + if (env.SLS_LOG_LEVEL !== 'debug' && argv.includes('--verbose')) { + env.SLS_LOG_LEVEL = 'info'; + } + + argv.some((flag, index) => { + const namespace = (() => { + if (flag === '--debug') return argv[index + 1]; + if (flag.startsWith('--debug=')) return flag.slice('--debug='.length); + return null; + })(); + if (!namespace) return false; + if (namespace === '*') env.SLS_LOG_LEVEL = 'debug'; + else env.SLS_LOG_DEBUG = namespace; + return true; + }); + + const logReporter = require('../lib/log-reporters/node/log-reporter'); + const { emitter: outputEmitter } = require('../lib/log/get-output-reporter'); + const joinTextTokens = require('../lib/log/join-text-tokens'); + const logLevels = require('log/levels'); + + const logLevelIndex = logLevels.includes(env.SLS_LOG_LEVEL) + ? logLevels.indexOf(env.SLS_LOG_LEVEL) + : logLevels.indexOf('notice'); + + const isInteractive = + (stdin.isTTY && stdout.isTTY && !env.CI) || env.SLS_INTERACTIVE_SETUP_ENABLE; + + require('../lib/log-reporters/node/style'); + + logReporter({ logLevelIndex, debugNamespaces: env.SLS_LOG_DEBUG }); + uniGlobal.logLevelIndex = logLevelIndex; + + outputEmitter.on('write', ({ mode, textTokens }) => { + if (mode === 'text') stdout.write(joinTextTokens(textTokens)); + }); + + uniGlobal.logIsInteractive = isInteractive; + if (isInteractive) { + require('../lib/log-reporters/node/progress-reporter')({ logLevelIndex }); + } + + return { logLevelIndex, isInteractive }; +}; diff --git a/test/lib/setup/log.js b/test/lib/setup/log.js deleted file mode 100644 index f921f04..0000000 --- a/test/lib/setup/log.js +++ /dev/null @@ -1,63 +0,0 @@ -'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; - -const logEmitter = require('log/lib/emitter'); - -const logsBuffer = []; -const flushLogs = () => { - if (logsBuffer.some((event) => event.logger.namespace !== 'mocha')) { - log.notice('flushing previously gathered logs...'); - logsBuffer.pop(); - logsBuffer.forEach((event) => { - if (!event.message) logWriter.resolveMessage(event); - logWriter.writeMessage(event); - }); - } - logsBuffer.length = 0; -}; - -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; - }); - runner.on('fail', (ignore, error) => { - log.error('test fail %s', error && error.stack); - logsBuffer.pop(); - flushLogs(); - }); -}); - -module.exports.flushLogs = flushLogs; diff --git a/test/lib/setup/mock-cwd.js b/test/lib/setup/mock-cwd.js deleted file mode 100644 index 3050a1f..0000000 --- a/test/lib/setup/mock-cwd.js +++ /dev/null @@ -1,16 +0,0 @@ -'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; - resetCwd(); - }); -}); diff --git a/test/lib/setup/mock-homedir.js b/test/lib/setup/mock-homedir.js deleted file mode 100644 index 9c8b2b3..0000000 --- a/test/lib/setup/mock-homedir.js +++ /dev/null @@ -1,43 +0,0 @@ -'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('../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; - - try { - emptyDirSync(tmpHomeDir); - } catch (error) { - if (rmTmpDirIgnorableErrorCodes.has(error.code)) return; - if (suite.tests.some((test) => test.timedOut)) return; - process.nextTick(() => { - throw error; - }); - } - }); -}); diff --git a/test/lib/setup/patch.js b/test/lib/setup/patch.js deleted file mode 100644 index 4bcde5a..0000000 --- a/test/lib/setup/patch.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const EventEmitter = require('events'); -const Mocha = require('mocha/lib/mocha'); - -process.on('unhandledRejection', (err) => { - process.stderr.write(`Unhandled rejection: ${err && err.stack}\n`); - throw err; -}); - -process.env.SLS_DEPRECATION_NOTIFICATION_MODE = 'error'; -process.env.SLS_TELEMETRY_DISABLED = '1'; - -const runnerEmitter = new EventEmitter(); -const mochaRun = Mocha.prototype.run; - -Mocha.prototype.run = function (...args) { - const runner = mochaRun.apply(this, args); - if (runner && runner.constructor && runner.constructor.name === 'Runner') { - runnerEmitter.emit('runner', runner); - } - return runner; -}; - -module.exports = { runnerEmitter }; diff --git a/test/lib/setup/restore-env.js b/test/lib/setup/restore-env.js deleted file mode 100644 index b613170..0000000 --- a/test/lib/setup/restore-env.js +++ /dev/null @@ -1,20 +0,0 @@ -'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; - 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/mocha/bootstrap.cjs b/test/mocha/bootstrap.cjs new file mode 100644 index 0000000..91fffb8 --- /dev/null +++ b/test/mocha/bootstrap.cjs @@ -0,0 +1,10 @@ +'use strict'; + +const chai = require('chai'); + +chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); + +process.env.SLS_DEPRECATION_NOTIFICATION_MODE ??= 'error'; +process.env.SLS_TELEMETRY_DISABLED = '1'; +process.env.LOG_TIME ??= 'abs'; diff --git a/test/mocha/root-hooks.cjs b/test/mocha/root-hooks.cjs new file mode 100644 index 0000000..90125b0 --- /dev/null +++ b/test/mocha/root-hooks.cjs @@ -0,0 +1,23 @@ +'use strict'; + +const sinon = require('sinon'); +const runtimeSandbox = require('./runtime-sandbox.cjs'); + +exports.mochaHooks = { + beforeAll() { + runtimeSandbox.enterSuite(); + }, + + beforeEach() { + runtimeSandbox.restoreTestState(); + }, + + afterEach() { + sinon.restore(); + runtimeSandbox.restoreTestState(); + }, + + afterAll() { + runtimeSandbox.exitSuite(); + }, +}; diff --git a/test/mocha/runtime-sandbox.cjs b/test/mocha/runtime-sandbox.cjs new file mode 100644 index 0000000..98a91cd --- /dev/null +++ b/test/mocha/runtime-sandbox.cjs @@ -0,0 +1,60 @@ +'use strict'; + +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const originalCwd = process.cwd(); +const originalEnv = Object.assign(Object.create(null), process.env); +const originalArgv = process.argv.slice(); +const originalComposeCommandStartTime = EvalError.$composeCommandStartTime; + +const restoreEnv = (targetEnv, sourceEnv) => { + for (const key of Object.keys(targetEnv)) { + if (!(key in sourceEnv)) delete targetEnv[key]; + } + + for (const [key, value] of Object.entries(sourceEnv)) { + targetEnv[key] = value; + } +}; + +class RuntimeSandbox { + constructor() { + this.sandboxPath = null; + this.sandboxEnv = null; + } + + enterSuite() { + this.sandboxPath = fs.mkdtempSync(path.join(os.tmpdir(), 'compose-test-')); + this.sandboxEnv = Object.assign(Object.create(null), process.env, { + HOME: this.sandboxPath, + USERPROFILE: this.sandboxPath, + }); + + this.restoreTestState(); + } + + restoreTestState() { + restoreEnv(process.env, this.sandboxEnv); + process.argv = originalArgv.slice(); + if (originalComposeCommandStartTime === undefined) delete EvalError.$composeCommandStartTime; + else EvalError.$composeCommandStartTime = originalComposeCommandStartTime; + process.chdir(this.sandboxPath); + } + + exitSuite() { + restoreEnv(process.env, originalEnv); + process.argv = originalArgv.slice(); + if (originalComposeCommandStartTime === undefined) delete EvalError.$composeCommandStartTime; + else EvalError.$composeCommandStartTime = originalComposeCommandStartTime; + process.chdir(originalCwd); + if (this.sandboxPath) { + fs.rmSync(this.sandboxPath, { recursive: true, force: true }); + this.sandboxPath = null; + this.sandboxEnv = null; + } + } +} + +module.exports = new RuntimeSandbox(); diff --git a/test/mocha/unit.cjs b/test/mocha/unit.cjs new file mode 100644 index 0000000..e12c231 --- /dev/null +++ b/test/mocha/unit.cjs @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + spec: ['test/**/*.test.js'], + require: ['./test/mocha/bootstrap.cjs', './test/mocha/root-hooks.cjs'], + timeout: 10000, + parallel: false, + 'node-option': ['unhandled-rejections=strict'], +}; diff --git a/test/unit/bin/serverless-compose.test.js b/test/unit/bin/serverless-compose.test.js index 6236603..d9f72ee 100644 --- a/test/unit/bin/serverless-compose.test.js +++ b/test/unit/bin/serverless-compose.test.js @@ -4,33 +4,22 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/bin/serverless-compose.test.js', () => { - let originalComposeCommandStartTime; - - beforeEach(() => { - originalComposeCommandStartTime = EvalError.$composeCommandStartTime; - }); - - afterEach(() => { - if (originalComposeCommandStartTime === undefined) { - delete EvalError.$composeCommandStartTime; - } else { - EvalError.$composeCommandStartTime = originalComposeCommandStartTime; - } - sinon.restore(); - }); - const loadBin = (stubs) => { delete require.cache[require.resolve('../../../bin/serverless-compose')]; proxyquire.noCallThru().load('../../../bin/serverless-compose', stubs); }; + afterEach(() => { + sinon.restore(); + }); + it('exits before loading the runtime on unsupported Node versions', () => { + process.argv = ['node', 'serverless-compose', 'deploy', '--verbose']; const isSupportedNodeVersion = sinon.stub().returns(false); + isSupportedNodeVersion.supportedRange = '^20.19.0 || >=22.12.0'; const runComponents = sinon.stub(); const stderrWrite = sinon.stub(process.stderr, 'write'); const processExitError = new Error('process.exit'); @@ -49,15 +38,17 @@ describe('test/unit/bin/serverless-compose.test.js', () => { expect(isSupportedNodeVersion).to.have.been.calledOnceWithExactly(process.version); expect(stderrWrite).to.have.been.calledOnceWithExactly( 'Error: Serverless Framework Compose v1.3.0 does not support ' + - `Node.js ${process.version}. Please upgrade Node.js to the latest ` + - 'LTS release. Minimum supported version: v20.0.0.\n' + `Node.js ${process.version}. Please use a supported release. ` + + 'Supported versions: ^20.19.0 || >=22.12.0.\n' ); expect(processExit).to.have.been.calledOnceWithExactly(1); expect(runComponents.called).to.equal(false); }); it('loads the runtime on supported Node versions', () => { + process.argv = ['node', 'serverless-compose', 'deploy', '--verbose']; const isSupportedNodeVersion = sinon.stub().returns(true); + isSupportedNodeVersion.supportedRange = '^20.19.0 || >=22.12.0'; const runComponents = sinon.stub().resolves(); const stderrWrite = sinon.stub(process.stderr, 'write'); const processExit = sinon.stub(process, 'exit'); @@ -68,18 +59,21 @@ describe('test/unit/bin/serverless-compose.test.js', () => { }); expect(isSupportedNodeVersion).to.have.been.calledOnceWithExactly(process.version); - expect(runComponents).to.have.been.calledOnceWithExactly(); + expect(runComponents).to.have.been.calledOnceWithExactly(['deploy', '--verbose']); expect(stderrWrite.called).to.equal(false); expect(processExit.called).to.equal(false); }); it('rethrows async runtime failures on nextTick', () => { + process.argv = ['node', 'serverless-compose', 'deploy']; const runtimeError = new Error('boom'); const nextTick = sinon.stub(process, 'nextTick').callsFake((handler) => handler()); expect(() => { loadBin({ - '../src/cli/is-supported-node-version': sinon.stub().returns(true), + '../src/cli/is-supported-node-version': Object.assign(sinon.stub().returns(true), { + supportedRange: '^20.19.0 || >=22.12.0', + }), '../src': { runComponents: sinon.stub().returns({ catch: (handler) => handler(runtimeError), diff --git a/test/unit/components/framework/index.test.js b/test/unit/components/framework/index.test.js index 8299c88..716c0ce 100644 --- a/test/unit/components/framework/index.test.js +++ b/test/unit/components/framework/index.test.js @@ -9,10 +9,7 @@ const { validateComponentInputs } = require('../../../../src/configuration/valid const { configSchema } = require('../../../../components/framework/configuration'); const ServerlessFramework = require('../../../../components/framework'); -// Configure chai -chai.use(require('chai-as-promised')); -chai.use(require('sinon-chai')); -const expect = require('chai').expect; +const expect = chai.expect; /** * @returns {Promise} diff --git a/test/unit/src/Context.test.js b/test/unit/src/Context.test.js index fa8fa94..f09548f 100644 --- a/test/unit/src/Context.test.js +++ b/test/unit/src/Context.test.js @@ -7,15 +7,16 @@ const Context = require('../../../src/Context'); const readStream = require('../read-stream'); describe('test/unit/src/Context.test.js', () => { - const contextConfig = { - root: process.cwd(), - stage: 'dev', - disableIO: true, - configuration: {}, - }; + const createContext = () => + new Context({ + root: process.cwd(), + stage: 'dev', + disableIO: true, + configuration: {}, + }); it('does not render empty or invalid outputs', async () => { - const context = new Context(contextConfig); + const context = createContext(); context.renderOutputs(null); context.renderOutputs({}); @@ -24,7 +25,7 @@ describe('test/unit/src/Context.test.js', () => { }); it('renders outputs through the configured output writer', async () => { - const context = new Context(contextConfig); + const context = createContext(); context.renderOutputs({ value: 1 }); diff --git a/test/unit/src/cli/is-supported-node-version.test.js b/test/unit/src/cli/is-supported-node-version.test.js index 2c22a0b..1490e2b 100644 --- a/test/unit/src/cli/is-supported-node-version.test.js +++ b/test/unit/src/cli/is-supported-node-version.test.js @@ -9,11 +9,23 @@ describe('test/unit/src/cli/is-supported-node-version.test.js', () => { expect(isSupportedNodeVersion('v18.20.0')).to.equal(false); }); - it('accepts Node 20', () => { - expect(isSupportedNodeVersion('v20.0.0')).to.equal(true); + it('rejects Node 20 releases before 20.19.0', () => { + expect(isSupportedNodeVersion('v20.18.1')).to.equal(false); }); - it('accepts Node 22', () => { - expect(isSupportedNodeVersion('v22.1.0')).to.equal(true); + it('accepts Node 20.19.0 and newer 20.x releases', () => { + expect(isSupportedNodeVersion('v20.19.0')).to.equal(true); + }); + + it('rejects unsupported odd-numbered majors', () => { + expect(isSupportedNodeVersion('v21.7.0')).to.equal(false); + }); + + it('rejects Node 22 releases before 22.12.0', () => { + expect(isSupportedNodeVersion('v22.11.0')).to.equal(false); + }); + + it('accepts Node 22.12.0 and newer releases', () => { + expect(isSupportedNodeVersion('v22.12.0')).to.equal(true); }); }); diff --git a/test/unit/src/components-service.test.js b/test/unit/src/components-service.test.js index 64c0386..7fb49d3 100644 --- a/test/unit/src/components-service.test.js +++ b/test/unit/src/components-service.test.js @@ -9,8 +9,6 @@ const Context = require('../../../src/Context'); const stripAnsi = require('strip-ansi'); const readStream = require('../read-stream'); -chai.use(require('chai-as-promised')); - const expect = chai.expect; const frameworkComponentPath = path.dirname( diff --git a/test/unit/src/configuration/read.test.js b/test/unit/src/configuration/read.test.js index 8b7f097..32897ac 100644 --- a/test/unit/src/configuration/read.test.js +++ b/test/unit/src/configuration/read.test.js @@ -3,7 +3,6 @@ const chai = require('chai'); const expect = chai.expect; -chai.use(require('chai-as-promised')); const path = require('path'); const fsp = require('fs').promises; diff --git a/test/unit/src/state/S3StateStorage.test.js b/test/unit/src/state/S3StateStorage.test.js index 35586d8..3dfd721 100644 --- a/test/unit/src/state/S3StateStorage.test.js +++ b/test/unit/src/state/S3StateStorage.test.js @@ -7,9 +7,6 @@ const stream = require('stream'); const S3StateStorage = require('../../../../src/state/S3StateStorage'); -chai.use(require('sinon-chai')); -chai.use(require('chai-as-promised')); - const expect = chai.expect; describe('test/unit/src/state/S3StateStorage.test.js', () => { diff --git a/test/unit/src/state/get-s3-state-storage-from-config.test.js b/test/unit/src/state/get-s3-state-storage-from-config.test.js index 0027c1e..400e725 100644 --- a/test/unit/src/state/get-s3-state-storage-from-config.test.js +++ b/test/unit/src/state/get-s3-state-storage-from-config.test.js @@ -4,8 +4,6 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/state/get-s3-state-storage-from-config.test.js', () => { diff --git a/test/unit/src/state/utils/get-state-bucket-name.test.js b/test/unit/src/state/utils/get-state-bucket-name.test.js index d805880..185dad5 100644 --- a/test/unit/src/state/utils/get-state-bucket-name.test.js +++ b/test/unit/src/state/utils/get-state-bucket-name.test.js @@ -14,9 +14,6 @@ const { const getStateBucketName = require('../../../../../src/state/utils/get-state-bucket-name'); const Context = require('../../../../../src/Context'); -chai.use(require('chai-as-promised')); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/state/utils/get-state-bucket-name.test.js', () => { diff --git a/test/unit/src/state/utils/get-state-bucket-region.test.js b/test/unit/src/state/utils/get-state-bucket-region.test.js index c931532..44f2de4 100644 --- a/test/unit/src/state/utils/get-state-bucket-region.test.js +++ b/test/unit/src/state/utils/get-state-bucket-region.test.js @@ -8,9 +8,6 @@ const sinon = require('sinon'); const getStateBucketRegion = require('../../../../../src/state/utils/get-state-bucket-region'); -chai.use(require('chai-as-promised')); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/state/utils/get-state-bucket-region.test.js', () => { diff --git a/test/unit/src/utils/aws/default-provider.test.js b/test/unit/src/utils/aws/default-provider.test.js index a5431d2..013698d 100644 --- a/test/unit/src/utils/aws/default-provider.test.js +++ b/test/unit/src/utils/aws/default-provider.test.js @@ -4,8 +4,6 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/utils/aws/default-provider.test.js', () => { diff --git a/test/unit/src/utils/aws/from-node-provider-chain.test.js b/test/unit/src/utils/aws/from-node-provider-chain.test.js index 694f997..8e37528 100644 --- a/test/unit/src/utils/aws/from-node-provider-chain.test.js +++ b/test/unit/src/utils/aws/from-node-provider-chain.test.js @@ -4,8 +4,6 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/utils/aws/from-node-provider-chain.test.js', () => { diff --git a/test/unit/src/utils/aws/get-client-config.test.js b/test/unit/src/utils/aws/get-client-config.test.js index d07211b..c5839d9 100644 --- a/test/unit/src/utils/aws/get-client-config.test.js +++ b/test/unit/src/utils/aws/get-client-config.test.js @@ -4,8 +4,6 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/utils/aws/get-client-config.test.js', () => { diff --git a/test/unit/src/utils/aws/get-credential-provider.test.js b/test/unit/src/utils/aws/get-credential-provider.test.js index 993360a..1430685 100644 --- a/test/unit/src/utils/aws/get-credential-provider.test.js +++ b/test/unit/src/utils/aws/get-credential-provider.test.js @@ -4,8 +4,6 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/utils/aws/get-credential-provider.test.js', () => { diff --git a/test/unit/src/utils/aws/remote-provider.test.js b/test/unit/src/utils/aws/remote-provider.test.js index f283bb3..3f02053 100644 --- a/test/unit/src/utils/aws/remote-provider.test.js +++ b/test/unit/src/utils/aws/remote-provider.test.js @@ -4,9 +4,6 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('chai-as-promised')); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/utils/aws/remote-provider.test.js', () => { diff --git a/test/unit/src/utils/serverless-utils/log-reporters/node.test.js b/test/unit/src/utils/serverless-utils/log-reporters/node.test.js index 1fd3818..2f68bf0 100644 --- a/test/unit/src/utils/serverless-utils/log-reporters/node.test.js +++ b/test/unit/src/utils/serverless-utils/log-reporters/node.test.js @@ -4,45 +4,20 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/utils/serverless-utils/log-reporters/node.test.js', () => { - let originalArgv; - let originalInteractiveSetup; - let originalCi; - let originalLogLevel; - let originalLogDebug; - - beforeEach(() => { - originalArgv = process.argv.slice(); - originalInteractiveSetup = process.env.SLS_INTERACTIVE_SETUP_ENABLE; - originalCi = process.env.CI; - originalLogLevel = process.env.SLS_LOG_LEVEL; - originalLogDebug = process.env.SLS_LOG_DEBUG; - }); - - afterEach(() => { - process.argv = originalArgv; - if (originalInteractiveSetup == null) delete process.env.SLS_INTERACTIVE_SETUP_ENABLE; - else process.env.SLS_INTERACTIVE_SETUP_ENABLE = originalInteractiveSetup; - if (originalCi == null) delete process.env.CI; - else process.env.CI = originalCi; - if (originalLogLevel == null) delete process.env.SLS_LOG_LEVEL; - else process.env.SLS_LOG_LEVEL = originalLogLevel; - if (originalLogDebug == null) delete process.env.SLS_LOG_DEBUG; - else process.env.SLS_LOG_DEBUG = originalLogDebug; - sinon.restore(); - }); - const loadModule = (uniGlobalState = {}, overrides = {}) => { const logReporter = overrides.logReporter || sinon.stub(); const progressReporter = overrides.progressReporter || sinon.stub(); const outputEmitter = overrides.outputEmitter || { on: sinon.stub() }; const joinTextTokens = overrides.joinTextTokens || sinon.stub().returns('joined'); + const env = overrides.env || {}; + const argv = overrides.argv || []; + const stdin = overrides.stdin || { isTTY: false }; + const stdout = overrides.stdout || { isTTY: false, write: sinon.stub() }; - proxyquire + const initializeNodeLogging = proxyquire .noCallThru() .load('../../../../../../src/utils/serverless-utils/log-reporters/node', { 'uni-global': () => uniGlobalState, @@ -54,33 +29,41 @@ describe('test/unit/src/utils/serverless-utils/log-reporters/node.test.js', () = '../lib/log-reporters/node/progress-reporter': progressReporter, }); - return { logReporter, progressReporter, outputEmitter, joinTextTokens }; + const result = initializeNodeLogging({ argv, env, stdin, stdout }); + + return { logReporter, progressReporter, outputEmitter, joinTextTokens, env, stdout, result }; }; it('sets SLS_LOG_LEVEL=info when verbose mode is enabled', () => { - const uniGlobalState = {}; - delete process.env.SLS_LOG_LEVEL; - process.argv = ['node', 'compose', '--verbose']; - - const { logReporter, outputEmitter } = loadModule(uniGlobalState); + const { env, logReporter, outputEmitter } = loadModule({}, { argv: ['deploy', '--verbose'] }); - expect(process.env.SLS_LOG_LEVEL).to.equal('info'); + expect(env.SLS_LOG_LEVEL).to.equal('info'); expect(logReporter).to.have.been.calledOnceWithExactly({ logLevelIndex: 1, debugNamespaces: undefined, }); - expect(uniGlobalState.logLevelIndex).to.equal(1); expect(outputEmitter.on).to.have.been.calledOnce; }); - it('does not override debug logging with verbose mode', () => { + it('stores the derived log level in shared state', () => { const uniGlobalState = {}; - process.env.SLS_LOG_LEVEL = 'debug'; - process.argv = ['node', 'compose', '--verbose']; - const { logReporter } = loadModule(uniGlobalState); + const { outputEmitter } = loadModule(uniGlobalState, { argv: ['deploy', '--verbose'] }); - expect(process.env.SLS_LOG_LEVEL).to.equal('debug'); + expect(uniGlobalState.logLevelIndex).to.equal(1); + expect(outputEmitter.on).to.have.been.calledOnce; + }); + + it('does not override debug logging with verbose mode', () => { + const { env, logReporter } = loadModule( + {}, + { + argv: ['deploy', '--verbose'], + env: { SLS_LOG_LEVEL: 'debug' }, + } + ); + + expect(env.SLS_LOG_LEVEL).to.equal('debug'); expect(logReporter).to.have.been.calledOnceWithExactly({ logLevelIndex: 0, debugNamespaces: undefined, @@ -89,23 +72,21 @@ describe('test/unit/src/utils/serverless-utils/log-reporters/node.test.js', () = it('is idempotent when reporter setup already happened', () => { const uniGlobalState = { logLevelIndex: 1 }; - process.argv = ['node', 'compose', '--verbose']; - const { logReporter, progressReporter, outputEmitter } = loadModule(uniGlobalState); + const { logReporter, progressReporter, outputEmitter, result } = loadModule(uniGlobalState, { + argv: ['deploy', '--verbose'], + }); expect(logReporter.called).to.equal(false); expect(progressReporter.called).to.equal(false); expect(outputEmitter.on.called).to.equal(false); + expect(result).to.deep.equal({ logLevelIndex: 1, isInteractive: undefined }); }); it('maps debug namespaces from argv into SLS_LOG_DEBUG', () => { - const uniGlobalState = {}; - delete process.env.SLS_LOG_DEBUG; - process.argv = ['node', 'compose', '--debug=aws']; + const { env, logReporter } = loadModule({}, { argv: ['deploy', '--debug=aws'] }); - const { logReporter } = loadModule(uniGlobalState); - - expect(process.env.SLS_LOG_DEBUG).to.equal('aws'); + expect(env.SLS_LOG_DEBUG).to.equal('aws'); expect(logReporter).to.have.been.calledOnceWithExactly({ logLevelIndex: 2, debugNamespaces: 'aws', @@ -114,9 +95,10 @@ describe('test/unit/src/utils/serverless-utils/log-reporters/node.test.js', () = it('registers the progress reporter when interactive setup is enabled', () => { const uniGlobalState = {}; - process.env.SLS_INTERACTIVE_SETUP_ENABLE = '1'; - const { progressReporter } = loadModule(uniGlobalState); + const { progressReporter } = loadModule(uniGlobalState, { + env: { SLS_INTERACTIVE_SETUP_ENABLE: '1' }, + }); expect(progressReporter).to.have.been.calledOnceWithExactly({ logLevelIndex: 2 }); expect(uniGlobalState.logIsInteractive).to.equal('1'); @@ -130,12 +112,12 @@ describe('test/unit/src/utils/serverless-utils/log-reporters/node.test.js', () = }), }; const joinTextTokens = sinon.stub().returns('joined\n'); - const stdoutWrite = sinon.stub(process.stdout, 'write'); + const stdout = { isTTY: false, write: sinon.stub() }; - loadModule({}, { outputEmitter, joinTextTokens }); + loadModule({}, { outputEmitter, joinTextTokens, stdout }); handlers.get('write')({ mode: 'text', textTokens: ['first', 'second'] }); expect(joinTextTokens).to.have.been.calledOnceWithExactly(['first', 'second']); - expect(stdoutWrite).to.have.been.calledOnceWithExactly('joined\n'); + expect(stdout.write).to.have.been.calledOnceWithExactly('joined\n'); }); }); diff --git a/test/unit/src/utils/serverless-utils/log-reporters/node/progress-reporter.test.js b/test/unit/src/utils/serverless-utils/log-reporters/node/progress-reporter.test.js index b666dc1..f352167 100644 --- a/test/unit/src/utils/serverless-utils/log-reporters/node/progress-reporter.test.js +++ b/test/unit/src/utils/serverless-utils/log-reporters/node/progress-reporter.test.js @@ -4,8 +4,6 @@ const chai = require('chai'); const proxyquire = require('proxyquire'); const sinon = require('sinon'); -chai.use(require('sinon-chai')); - const expect = chai.expect; describe('test/unit/src/utils/serverless-utils/log-reporters/node/progress-reporter.test.js', () => { From f7d138405f93ee1f698be855de92f5ae256dc15c Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 22 Apr 2026 15:31:02 +0100 Subject: [PATCH 2/2] CI cleanup --- .github/workflows/publish.yml | 11 +++++++---- .github/workflows/tests.yml | 33 +++++++++++++++------------------ .gitignore | 4 ++-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9c7faf1..8b5eb0f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,21 +16,23 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Retrieve dependencies from cache + - name: Restore npm cache uses: actions/cache@v5 - id: cacheNpm with: path: ~/.npm - key: npm-v22-${{ runner.os }}-refs/heads/main-${{ hashFiles('package-lock.json') }} + key: npm-no-lock-v1-${{ runner.os }}-node22-${{ hashFiles('package.json') }} + restore-keys: | + npm-no-lock-v1-${{ runner.os }}-node22- - name: Install Node.js and npm uses: actions/setup-node@v6 with: node-version: 22.x + package-manager-cache: false registry-url: https://registry.npmjs.org - name: Install dependencies - run: npm ci + run: npm install --package-lock=false # Store the name of the release # See https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions @@ -73,6 +75,7 @@ jobs: uses: actions/setup-node@v6 with: node-version: 22.x + package-manager-cache: false registry-url: https://registry.npmjs.org - name: Publish new version diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3c05ceb..e3211e9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,23 +26,22 @@ jobs: git checkout main git checkout pr - - name: Retrieve dependencies from cache - id: cacheNpm + - name: Restore npm cache uses: actions/cache@v5 with: path: ~/.npm - key: npm-v22-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package-lock.json') }} + key: npm-no-lock-v1-${{ runner.os }}-node22-${{ hashFiles('package.json') }} restore-keys: | - npm-v22-${{ runner.os }}-${{ github.ref }}- - npm-v22-${{ runner.os }}-refs/heads/main- + npm-no-lock-v1-${{ runner.os }}-node22- - name: Install Node.js and npm uses: actions/setup-node@v6 with: node-version: 22.x + package-manager-cache: false - name: Install dependencies - run: npm ci + run: npm install --package-lock=false - name: Validate formatting run: npm run prettier-check:updated @@ -66,23 +65,22 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Retrieve dependencies from cache - id: cacheNpm + - name: Restore npm cache uses: actions/cache@v5 with: path: ~/.npm - key: npm-v22-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package-lock.json') }} + key: npm-no-lock-v1-${{ runner.os }}-node22-${{ hashFiles('package.json') }} restore-keys: | - npm-v22-${{ runner.os }}-${{ github.ref }}- - npm-v22-${{ runner.os }}-refs/heads/main- + npm-no-lock-v1-${{ runner.os }}-node22- - name: Install Node.js and npm uses: actions/setup-node@v6 with: node-version: 22.x + package-manager-cache: false - name: Install dependencies - run: npm ci + run: npm install --package-lock=false - name: Unit tests run: npm test @@ -94,23 +92,22 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Retrieve dependencies from cache - id: cacheNpm + - name: Restore npm cache uses: actions/cache@v5 with: path: ~/.npm - key: npm-v20-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package-lock.json') }} + key: npm-no-lock-v1-${{ runner.os }}-node20-${{ hashFiles('package.json') }} restore-keys: | - npm-v20-${{ runner.os }}-${{ github.ref }}- - npm-v20-${{ runner.os }}-refs/heads/main- + npm-no-lock-v1-${{ runner.os }}-node20- - name: Install Node.js and npm uses: actions/setup-node@v6 with: node-version: 20.x + package-manager-cache: false - name: Install dependencies - run: npm ci + run: npm install --package-lock=false - name: Unit tests run: npm test diff --git a/.gitignore b/.gitignore index 282a601..2eb8a2e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ /coverage /dist npm-debug.log -package-lock.json -yarn.lock +/package-lock.json +/yarn.lock node_modules .serverless