diff --git a/package.json b/package.json index 3744b5a..2b0d962 100644 --- a/package.json +++ b/package.json @@ -34,21 +34,21 @@ "@aws-sdk/property-provider": "^3.366.0", "@dagrejs/graphlib": "^3.0.4", "ajv": "^8.11.0", - "cli-cursor": "^3", + "cli-cursor": "^5.0.0", "cli-progress-footer": "^2.3.2", "cross-spawn": "^7.0.6", "d": "^1.0.1", "event-emitter": "^0.3.5", "ext": "^1.7.0", "fast-glob": "^3.3.3", - "fs-extra": "^10.1.0", + "fs-extra": "^11.3.4", "js-yaml": "^4.1.0", "log": "^6.3.1", "log-node": "^8.0.3", "memoizee": "^0.4.15", "minimist": "^1.2.6", "semver": "^7.3.7", - "signal-exit": "^3.0.7", + "signal-exit": "^4.1.0", "traverse": "^0.6.6", "type": "^2.6.1", "uni-global": "^1.0.0" diff --git a/src/cli/Progresses.js b/src/cli/Progresses.js index c95bff3..cfc2d3a 100644 --- a/src/cli/Progresses.js +++ b/src/cli/Progresses.js @@ -6,7 +6,7 @@ * https://github.com/jcarpanelli/spinnies/blob/master/LICENSE */ -const cliCursor = require('cli-cursor'); +const cliCursor = require('cli-cursor').default; const { stderrCliColors: colors } = require('./colors'); const symbols = require('./symbols'); const isUnicodeSupported = require('./is-unicode-supported'); diff --git a/src/index.js b/src/index.js index 8650136..18fa68f 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ const readConfiguration = require('./configuration/read'); const { validateConfiguration } = require('./configuration/validate'); const validateOptions = require('./validate-options'); const initializeNodeLogging = require('./utils/serverless-utils/log-reporters/node'); +const { signals } = require('signal-exit/signals'); let options; let method; @@ -24,7 +25,7 @@ process.once('uncaughtException', (error) => { handleError(error, usedContext.output); }); -require('signal-exit/signals').forEach((signal) => { +signals.forEach((signal) => { process.once(signal, () => { // If there's another listener (e.g. we're in deamon context or reading stdin input) // then let the other listener decide how process will exit diff --git a/test/unit/src/cli/Progresses.test.js b/test/unit/src/cli/Progresses.test.js index aa47471..ef91599 100644 --- a/test/unit/src/cli/Progresses.test.js +++ b/test/unit/src/cli/Progresses.test.js @@ -4,15 +4,38 @@ const expect = require('chai').expect; const proxyquire = require('proxyquire'); const sinon = require('sinon'); -const Progresses = require('../../../../src/cli/Progresses'); - describe('test/unit/src/cli/Progresses.test.js', () => { - const loadProgresses = (isUnicodeSupported) => - proxyquire.noCallThru().load('../../../../src/cli/Progresses', { + let loadedProgresses = []; + + const createCliCursorStub = () => ({ + show: sinon.stub(), + hide: sinon.stub(), + }); + + const loadProgresses = (isUnicodeSupported = true, cliCursor = createCliCursorStub()) => { + const Progresses = proxyquire.noCallThru().load('../../../../src/cli/Progresses', { + 'cli-cursor': { default: cliCursor }, './is-unicode-supported': () => isUnicodeSupported, }); + loadedProgresses.push(Progresses); + + return { Progresses, cliCursor }; + }; + + afterEach(() => { + for (const Progresses of loadedProgresses) { + if (Progresses.sigintHandler) { + process.removeListener('SIGINT', Progresses.sigintHandler); + } + delete Progresses.sigintHandler; + delete Progresses.lastBoundInstance; + } + loadedProgresses = []; + sinon.restore(); + }); const createProgresses = (columns = 5, rows = 3) => { + const { Progresses } = loadProgresses(); const progresses = Object.create(Progresses.prototype); progresses.output = { interactiveStderr: { columns, rows }, @@ -41,6 +64,7 @@ describe('test/unit/src/cli/Progresses.test.js', () => { }); it('tracks progresses in a null-prototype registry', () => { + const { Progresses } = loadProgresses(); const progresses = Object.create(Progresses.prototype); progresses.output = { interactiveStderr: null, @@ -59,39 +83,50 @@ describe('test/unit/src/cli/Progresses.test.js', () => { }); it('uses dots spinner when unicode is supported', () => { - const LocalProgresses = loadProgresses(true); - const bindSigintStub = sinon.stub(LocalProgresses.prototype, 'bindSigint'); - - try { - const progresses = new LocalProgresses({}); - - expect(progresses.options.spinner.frames).to.deep.equal([ - '⠋', - '⠙', - '⠹', - '⠸', - '⠼', - '⠴', - '⠦', - '⠧', - '⠇', - '⠏', - ]); - } finally { - bindSigintStub.restore(); - } + const { Progresses: LocalProgresses } = loadProgresses(true); + sinon.stub(LocalProgresses.prototype, 'bindSigint'); + + const progresses = new LocalProgresses({}); + + expect(progresses.options.spinner.frames).to.deep.equal([ + '⠋', + '⠙', + '⠹', + '⠸', + '⠼', + '⠴', + '⠦', + '⠧', + '⠇', + '⠏', + ]); }); it('uses dashes spinner when unicode is not supported', () => { - const LocalProgresses = loadProgresses(false); - const bindSigintStub = sinon.stub(LocalProgresses.prototype, 'bindSigint'); + const { Progresses: LocalProgresses } = loadProgresses(false); + sinon.stub(LocalProgresses.prototype, 'bindSigint'); - try { - const progresses = new LocalProgresses({}); + const progresses = new LocalProgresses({}); - expect(progresses.options.spinner.frames).to.deep.equal(['-', '_']); - } finally { - bindSigintStub.restore(); - } + expect(progresses.options.spinner.frames).to.deep.equal(['-', '_']); + }); + + it('restores cursor and exits through SIGINT handler', () => { + const { Progresses: LocalProgresses, cliCursor } = loadProgresses(); + const processExit = sinon.stub(process, 'exit'); + const moveCursor = sinon.stub(); + + const progresses = new LocalProgresses({ + interactiveStderr: { moveCursor }, + verbose: sinon.stub(), + }); + progresses.lineCount = 2; + + process.removeListener('SIGINT', LocalProgresses.sigintHandler); + LocalProgresses.sigintHandler(); + + expect(cliCursor.show).to.have.been.calledOnceWithExactly(); + expect(moveCursor).to.have.been.calledOnceWithExactly(0, 2); + expect(processExit).to.have.been.calledOnceWithExactly(0); }); }); diff --git a/test/unit/src/index.test.js b/test/unit/src/index.test.js index 50fa5dc..d8ee416 100644 --- a/test/unit/src/index.test.js +++ b/test/unit/src/index.test.js @@ -7,7 +7,26 @@ const sinon = require('sinon'); const expect = chai.expect; describe('test/unit/src/index.test.js', () => { + const moduleSignals = ['SIGINT', 'SIGTERM']; + const moduleEvents = ['uncaughtException', ...moduleSignals]; + let listenerSnapshots = []; + + const snapshotListeners = () => + new Map(moduleEvents.map((event) => [event, process.listeners(event)])); + + const restoreAddedListeners = (snapshot) => { + for (const [event, previousListeners] of snapshot) { + for (const listener of process.listeners(event)) { + if (!previousListeners.includes(listener)) { + process.removeListener(event, listener); + } + } + } + }; + afterEach(() => { + for (const snapshot of listenerSnapshots) restoreAddedListeners(snapshot); + listenerSnapshots = []; sinon.restore(); }); @@ -36,8 +55,11 @@ describe('test/unit/src/index.test.js', () => { ComponentsService.onCall(index).returns(instance); }); + const listenerSnapshot = snapshotListeners(); + listenerSnapshots.push(listenerSnapshot); delete require.cache[require.resolve('../../../src/index.js')]; - const { runComponents } = proxyquire('../../../src', { + const { runComponents } = proxyquire.noCallThru().load('../../../src', { + 'signal-exit/signals': { signals: moduleSignals }, './render-help': sinon.stub().resolves(), './Context': FakeContext, './ComponentsService': ComponentsService,