From 9d2d8ee93b74dc83f35298ce2a8fd4e23e6b602a Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Mon, 6 Dec 2021 15:40:29 +0100 Subject: [PATCH] serial-service tests --- arduino-ide-extension/package.json | 10 +- .../serial/serial-connection-manager.ts | 1 - .../src/node/serial/serial-service-impl.ts | 69 +++++--- .../browser/serial-connection-manager.test.ts | 7 + .../src/test/node/serial-service-impl.test.ts | 166 ++++++++++++++++++ yarn.lock | 99 +++++++---- 6 files changed, 287 insertions(+), 65 deletions(-) create mode 100644 arduino-ide-extension/src/test/node/serial-service-impl.test.ts diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 9a46e057e..47b28fbf4 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -19,12 +19,11 @@ "test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\"" }, "dependencies": { - "arduino-serial-plotter-webapp": "0.0.15", "@grpc/grpc-js": "^1.3.7", "@theia/application-package": "1.19.0", "@theia/core": "1.19.0", "@theia/editor": "1.19.0", - "@theia/editor-preview": "1.19.0", + "@theia/editor-preview": "1.19.0", "@theia/filesystem": "1.19.0", "@theia/git": "1.19.0", "@theia/keymaps": "1.19.0", @@ -53,10 +52,10 @@ "@types/ps-tree": "^1.1.0", "@types/react-select": "^3.0.0", "@types/react-tabs": "^2.3.2", - "@types/sinon": "^7.5.2", "@types/temp": "^0.8.34", "@types/which": "^1.3.1", "ajv": "^6.5.3", + "arduino-serial-plotter-webapp": "0.0.15", "async-mutex": "^0.3.0", "atob": "^2.1.2", "auth0-js": "^9.14.0", @@ -97,6 +96,8 @@ "@types/chai-string": "^1.4.2", "@types/mocha": "^5.2.7", "@types/react-window": "^1.8.5", + "@types/sinon": "^10.0.6", + "@types/sinon-chai": "^3.2.6", "chai": "^4.2.0", "chai-string": "^1.5.0", "decompress": "^4.2.0", @@ -109,7 +110,8 @@ "moment": "^2.24.0", "protoc": "^1.0.4", "shelljs": "^0.8.3", - "sinon": "^9.0.1", + "sinon": "^12.0.1", + "sinon-chai": "^3.7.0", "typemoq": "^2.1.0", "uuid": "^3.2.1", "yargs": "^11.1.0" diff --git a/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts b/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts index 0c2034154..5c29cf566 100644 --- a/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts +++ b/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts @@ -169,7 +169,6 @@ export class SerialConnectionManager { this.messageService.error( `Please select a board and a port to open the serial connection.` ); - return; } if (!this.webSocket && this.wsPort) { diff --git a/arduino-ide-extension/src/node/serial/serial-service-impl.ts b/arduino-ide-extension/src/node/serial/serial-service-impl.ts index c226e934e..7b288ac10 100644 --- a/arduino-ide-extension/src/node/serial/serial-service-impl.ts +++ b/arduino-ide-extension/src/node/serial/serial-service-impl.ts @@ -63,16 +63,6 @@ namespace ErrorWithCode { @injectable() export class SerialServiceImpl implements SerialService { - @named(SerialServiceName) - @inject(ILogger) - protected readonly logger: ILogger; - - @inject(MonitorClientProvider) - protected readonly serialClientProvider: MonitorClientProvider; - - @inject(WebSocketService) - protected readonly webSocketService: WebSocketService; - protected theiaFEClient?: SerialServiceClient; protected serialConfig?: SerialConfig; @@ -88,6 +78,18 @@ export class SerialServiceImpl implements SerialService { uploadInProgress = false; + constructor( + @inject(ILogger) + @named(SerialServiceName) + protected readonly logger: ILogger, + + @inject(MonitorClientProvider) + protected readonly serialClientProvider: MonitorClientProvider, + + @inject(WebSocketService) + protected readonly webSocketService: WebSocketService + ) {} + async isSerialPortOpen(): Promise { return !!this.serialConnection; } @@ -115,7 +117,6 @@ export class SerialServiceImpl implements SerialService { public async connectSerialIfRequired(): Promise { if (this.uploadInProgress) return; const clients = await this.clientsAttached(); - this.logger.info(`WS clients: ${clients}`); clients > 0 ? await this.connect() : await this.disconnect(); } @@ -144,7 +145,7 @@ export class SerialServiceImpl implements SerialService { this.webSocketService.sendMessage(JSON.stringify(msg)); } - async connect(): Promise { + private async connect(): Promise { if (!this.serialConfig) { return Status.CONFIG_MISSING; } @@ -155,8 +156,6 @@ export class SerialServiceImpl implements SerialService { )} on port ${Port.toString(this.serialConfig.port)}...` ); - // check if the board/port is available - if (this.serialConnection) { return Status.ALREADY_CONNECTED; } @@ -187,7 +186,6 @@ export class SerialServiceImpl implements SerialService { // Log the original, unexpected error. this.logger.error(error); } - // }); }).bind(this) ); @@ -259,9 +257,19 @@ export class SerialServiceImpl implements SerialService { } req.setConfig(monitorConfig); - return new Promise((resolve) => { - if (this.serialConnection) { - this.serialConnection.duplex.write(req, () => { + if (!this.serialConnection) { + return await this.disconnect(); + } + + const writeTimeout = new Promise((resolve) => { + setTimeout(async () => { + resolve(Status.NOT_CONNECTED); + }, 1000); + }); + + const writePromise = (serialConnection: any) => { + return new Promise((resolve) => { + serialConnection.duplex.write(req, () => { const boardName = this.serialConfig?.board ? Board.toString(this.serialConfig.board, { useFqbn: false, @@ -276,14 +284,23 @@ export class SerialServiceImpl implements SerialService { ); resolve(Status.OK); }); - return; - } - this.disconnect().then(() => resolve(Status.NOT_CONNECTED)); - }); + }); + }; + + const status = await Promise.race([ + writeTimeout, + writePromise(this.serialConnection), + ]); + + if (status === Status.NOT_CONNECTED) { + this.disconnect(); + } + + return status; } public async disconnect(reason?: SerialError): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { try { if (this.onMessageReceived) { this.onMessageReceived.dispose(); @@ -299,12 +316,14 @@ export class SerialServiceImpl implements SerialService { reason && reason.code === SerialError.ErrorCodes.CLIENT_CANCEL ) { - return Status.OK; + resolve(Status.OK); + return; } this.logger.info('>>> Disposing serial connection...'); if (!this.serialConnection) { this.logger.warn('<<< Not connected. Nothing to dispose.'); - return Status.NOT_CONNECTED; + resolve(Status.NOT_CONNECTED); + return; } const { duplex, config } = this.serialConnection; diff --git a/arduino-ide-extension/src/test/browser/serial-connection-manager.test.ts b/arduino-ide-extension/src/test/browser/serial-connection-manager.test.ts index 274a30a93..4f4d6c778 100644 --- a/arduino-ide-extension/src/test/browser/serial-connection-manager.test.ts +++ b/arduino-ide-extension/src/test/browser/serial-connection-manager.test.ts @@ -100,6 +100,13 @@ // return { dispose: () => {} }; // }); +// serialServiceClient +// .setup((mock) => mock.onWebSocketChanged(It.isAny())) +// .returns((h) => { +// handleWebSocketChanged = h; +// return { dispose: () => {} }; +// }); + // serialService // .setup((m) => m.disconnect()) // .returns(() => Promise.resolve(Status.OK)); diff --git a/arduino-ide-extension/src/test/node/serial-service-impl.test.ts b/arduino-ide-extension/src/test/node/serial-service-impl.test.ts new file mode 100644 index 000000000..f2dddcdd2 --- /dev/null +++ b/arduino-ide-extension/src/test/node/serial-service-impl.test.ts @@ -0,0 +1,166 @@ +import { SerialServiceImpl } from './../../node/serial/serial-service-impl'; +import { IMock, It, Mock } from 'typemoq'; +import { createSandbox } from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import { expect, use } from 'chai'; +use(sinonChai); + +import { ILogger } from '@theia/core/lib/common/logger'; +import { MonitorClientProvider } from '../../node/serial/monitor-client-provider'; +import { WebSocketService } from '../../node/web-socket/web-socket-service'; +import { MonitorServiceClient } from '../../node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb'; + +describe.only('SerialServiceImpl', () => { + let subject: SerialServiceImpl; + + let logger: IMock; + let serialClientProvider: IMock; + let webSocketService: IMock; + + beforeEach(() => { + logger = Mock.ofType(); + logger.setup((b) => b.info(It.isAnyString())); + logger.setup((b) => b.warn(It.isAnyString())); + logger.setup((b) => b.error(It.isAnyString())); + + serialClientProvider = Mock.ofType(); + webSocketService = Mock.ofType(); + + subject = new SerialServiceImpl( + logger.object, + serialClientProvider.object, + webSocketService.object + ); + }); + + // context('when a serial connection is requested', () => { + // const sandbox = createSandbox(); + // beforeEach(() => { + // subject.uploadInProgress = false; + // sandbox.spy(subject, 'disconnect'); + // sandbox.spy(subject, 'updateWsConfigParam'); + // }); + + // afterEach(function () { + // sandbox.restore(); + // }); + + // context('and an upload is in progress', () => { + // beforeEach(async () => { + // subject.uploadInProgress = true; + // }); + + // it('should not change the connection status', async () => { + // await subject.connectSerialIfRequired(); + // expect(subject.disconnect).to.have.callCount(0); + // }); + // }); + + // context('and there is no upload in progress', () => { + // beforeEach(async () => { + // subject.uploadInProgress = false; + // }); + + // context('and there are 0 attached ws clients', () => { + // it('should disconnect', async () => { + // await subject.connectSerialIfRequired(); + // expect(subject.disconnect).to.have.been.calledOnce; + // }); + // }); + + // context('and there are > 0 attached ws clients', () => { + // beforeEach(() => { + // webSocketService + // .setup((b) => b.getConnectedClientsNumber()) + // .returns(() => 1); + // }); + + // it('should not call the disconenct', async () => { + // await subject.connectSerialIfRequired(); + // expect(subject.disconnect).to.have.callCount(0); + // }); + // }); + // }); + // }); + + // context('when a disconnection is requested', () => { + // const sandbox = createSandbox(); + // beforeEach(() => {}); + + // afterEach(function () { + // sandbox.restore(); + // }); + + // context('and a serialConnection is not set', () => { + // it('should return a NOT_CONNECTED status', async () => { + // const status = await subject.disconnect(); + // expect(status).to.be.equal(Status.NOT_CONNECTED); + // }); + // }); + + // context('and a serialConnection is set', async () => { + // beforeEach(async () => { + // sandbox.spy(subject, 'updateWsConfigParam'); + // await subject.disconnect(); + // }); + + // it('should dispose the serialConnection', async () => { + // const serialConnectionOpen = await subject.isSerialPortOpen(); + // expect(serialConnectionOpen).to.be.false; + // }); + + // it('should call updateWsConfigParam with disconnected status', async () => { + // expect(subject.updateWsConfigParam).to.be.calledWith({ + // connected: false, + // }); + // }); + // }); + // }); + + context('when a new config is passed in', () => { + const sandbox = createSandbox(); + beforeEach(async () => { + subject.uploadInProgress = false; + webSocketService + .setup((b) => b.getConnectedClientsNumber()) + .returns(() => 1); + + serialClientProvider + .setup((b) => b.client()) + .returns(async () => { + return { + streamingOpen: () => { + return { + on: (str: string, cb: any) => {}, + write: (chunk: any, cb: any) => { + cb(); + }, + cancel: () => {}, + }; + }, + } as MonitorServiceClient; + }); + + sandbox.spy(subject, 'disconnect'); + + await subject.setSerialConfig({ + board: { name: 'test' }, + port: { address: 'test', protocol: 'test' }, + }); + }); + + afterEach(function () { + sandbox.restore(); + subject.dispose(); + }); + + // it('should disconnect from previous connection', async () => { + // expect(subject.disconnect).to.be.called; + // }); + + it('should create the serialConnection', async () => { + const serialConnectionOpen = await subject.isSerialPortOpen(); + expect(serialConnectionOpen).to.be.true; + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index b6cdcfa7f..fe560582c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2064,24 +2064,38 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== -"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1": +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0": version "1.8.2" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.2.tgz#858f5c4b48d80778fde4b9d541f27edc0d56488b" integrity sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw== dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" - integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== +"@sinonjs/commons@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^7.0.4", "@sinonjs/fake-timers@^7.1.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5" + integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg== dependencies: "@sinonjs/commons" "^1.7.0" -"@sinonjs/samsam@^5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.3.1.tgz#375a45fe6ed4e92fca2fb920e007c48232a6507f" - integrity sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg== +"@sinonjs/fake-timers@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/samsam@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.0.2.tgz#a0117d823260f282c04bff5f8704bdc2ac6910bb" + integrity sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ== dependencies: "@sinonjs/commons" "^1.6.0" lodash.get "^4.4.2" @@ -3246,16 +3260,26 @@ "@types/mime" "^1" "@types/node" "*" +"@types/sinon-chai@^3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.6.tgz#3504a744e2108646394766fb1339f52ea5d6bd0f" + integrity sha512-Z57LprQ+yOQNu9d6mWdHNvnmncPXzDWGSeLj+8L075/QahToapC4Q13zAFRVKV4clyBmdJ5gz4xBfVkOso5lXw== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + +"@types/sinon@*", "@types/sinon@^10.0.6": + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.6.tgz#bc3faff5154e6ecb69b797d311b7cf0c1b523a1d" + integrity sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg== + dependencies: + "@sinonjs/fake-timers" "^7.1.0" + "@types/sinon@^2.3.5": version "2.3.7" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.7.tgz#e92c2fed3297eae078d78d1da032b26788b4af86" integrity sha512-w+LjztaZbgZWgt/y/VMP5BUAWLtSyoIJhXyW279hehLPyubDoBNwvhcj3WaSptcekuKYeTCVxrq60rdLc6ImJA== -"@types/sinon@^7.5.2": - version "7.5.2" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.2.tgz#5e2f1d120f07b9cda07e5dedd4f3bf8888fccdb9" - integrity sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg== - "@types/tar-fs@^1.16.1": version "1.16.3" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.3.tgz#425b2b817c405d13d051f36ec6ec6ebd25e31069" @@ -6075,10 +6099,10 @@ diff@3.5.0, diff@^3.4.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -diff@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== dir-glob@^2.0.0, dir-glob@^2.2.2: version "2.2.2" @@ -10214,13 +10238,13 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -nise@^4.0.4: - version "4.1.0" - resolved "https://registry.yarnpkg.com/nise/-/nise-4.1.0.tgz#8fb75a26e90b99202fa1e63f448f58efbcdedaf6" - integrity sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA== +nise@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.0.tgz#713ef3ed138252daef20ec035ab62b7a28be645c" + integrity sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ== dependencies: "@sinonjs/commons" "^1.7.0" - "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/fake-timers" "^7.0.4" "@sinonjs/text-encoding" "^0.7.1" just-extend "^4.0.2" path-to-regexp "^1.7.0" @@ -12881,17 +12905,22 @@ simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" -sinon@^9.0.1: - version "9.2.4" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.4.tgz#e55af4d3b174a4443a8762fa8421c2976683752b" - integrity sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg== - dependencies: - "@sinonjs/commons" "^1.8.1" - "@sinonjs/fake-timers" "^6.0.1" - "@sinonjs/samsam" "^5.3.1" - diff "^4.0.2" - nise "^4.0.4" - supports-color "^7.1.0" +sinon-chai@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" + integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== + +sinon@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-12.0.1.tgz#331eef87298752e1b88a662b699f98e403c859e9" + integrity sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg== + dependencies: + "@sinonjs/commons" "^1.8.3" + "@sinonjs/fake-timers" "^8.1.0" + "@sinonjs/samsam" "^6.0.2" + diff "^5.0.0" + nise "^5.1.0" + supports-color "^7.2.0" slash@^1.0.0: version "1.0.0" @@ -13511,7 +13540,7 @@ supports-color@^5.3.0, supports-color@^5.4.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==