diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..ae3962c --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,75 @@ +# Javascript Node CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-javascript/ for more details +# +version: 2 +jobs: + build: + working_directory: ~/node-catchie + docker: + - image: circleci/node:8.9 + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + steps: + - checkout + - run: + name: Install make + command: sudo apt-get update && sudo apt-get -y install gcc make + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ .Branch }}-{{ checksum "package.json" }} + - v1-dependencies-{{ .Branch }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies + - run: + name: Install dependencies + command: make deps + - save_cache: + paths: + - node_modules + key: v1-dependencies-{{ .Branch }}-{{ checksum "package.json" }} + - run: + name: Test project + command: make test + + # Special step used to persist a temporary file to be used by another job in the workflow + - persist_to_workspace: + root: ~/node-catchie + paths: + - coverage + + sonarqube: + working_directory: ~/node-catchie + docker: + # Sonarqube need OpenJDK 8 to run his analysis correctly + - image: circleci/openjdk:8-jdk-browsers + steps: + - checkout + # Special step used to attach the workflow’s workspace to the current container + # Retrieve coverage's folder for sonarqube + - attach_workspace: + at: ~/node-catchie + + # Sonarqube need to have node installed to run his analysis + - run: + name: Install node + command: | + sudo apt install curl + curl -sL https://deb.nodesource.com/setup_10.x | sudo bash - + sudo apt install nodejs + - run: + name: Sonar analysis + command: make sonar + +workflows: + version: 2 + build_and_test: + jobs: + - build + - sonarqube: + # sonarqube's job waiting for build's job before to run + requires: + - build diff --git a/.eslintignore b/.eslintignore index b6f11ad..8a3acef 100644 --- a/.eslintignore +++ b/.eslintignore @@ -14,6 +14,9 @@ lib-cov coverage .sonar/ +# Nyc coverage directory +.nyc_output + # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/.gitignore b/.gitignore index aee5b0b..2c92f71 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,10 @@ pids # Directory for instrumented libs generated by jscoverage/JSCover lib-cov -# Coverage directory used by tools like istanbul +# Coverage directory used by tools like istanbul/nyc or sonar coverage .sonar/ +.nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/.npmignore b/.npmignore index 2fe96d7..9341f0f 100644 --- a/.npmignore +++ b/.npmignore @@ -10,8 +10,9 @@ pids # Directory for instrumented libs generated by jscoverage/JSCover lib-cov -# Coverage directory used by tools like istanbul +# Coverage directory used by tools like istanbul or nyc coverage +.nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/Makefile b/Makefile index 0b1ee22..f979936 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,29 @@ .PHONY: test deps: - npm i + npm install -lint: - node_modules/.bin/eslint . +run: + npm start -test: - make lint - make cover +lint: + npm run lint cover: - node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- test --recursive --timeout=10000 + npm run cover + +test: + npm run test sonar: sed '/sonar.projectVersion/d' ./sonar-project.properties > tmp && mv tmp sonar-project.properties echo sonar.projectVersion=`cat package.json | python -c "import json,sys;obj=json.load(sys.stdin);print obj['version'];"` >> sonar-project.properties - wget https://sonarsource.bintray.com/Distribution/sonar-scanner-cli/sonar-scanner-2.8.zip - unzip sonar-scanner-2.8.zip -ifdef CI_PULL_REQUEST - @sonar-scanner-2.8/bin/sonar-runner -e -Dsonar.analysis.mode=preview -Dsonar.github.pullRequest=${shell basename $(CI_PULL_REQUEST)} -Dsonar.github.repository=$(REPO_SLUG) -Dsonar.github.oauth=$(GITHUB_TOKEN) -Dsonar.login=$(SONAR_LOGIN) -Dsonar.password=$(SONAR_PASS) -Dsonar.host.url=$(SONAR_HOST_URL) + wget https://s3.eu-central-1.amazonaws.com/dialonce-cdn/utilities/sonar-scanner-cli.zip + unzip sonar-scanner-* +ifdef CIRCLE_PULL_REQUEST + @sonar-scanner/bin/sonar-scanner -e -Dsonar.analysis.mode=preview -Dsonar.github.pullRequest=${shell basename $(CIRCLE_PULL_REQUEST)} -Dsonar.github.repository=$(REPO_SLUG) -Dsonar.github.oauth=$(GITHUB_TOKEN) -Dsonar.login=$(SONAR_LOGIN) -Dsonar.password=$(SONAR_PASS) -Dsonar.host.url=$(SONAR_HOST_URL) endif ifeq ($(CIRCLE_BRANCH),develop) - @sonar-scanner-2.8/bin/sonar-runner -e -Dsonar.analysis.mode=publish -Dsonar.host.url=$(SONAR_HOST_URL) -Dsonar.login=$(SONAR_LOGIN) -Dsonar.password=$(SONAR_PASS) + @sonar-scanner/bin/sonar-scanner -e -Dsonar.analysis.mode=publish -Dsonar.host.url=$(SONAR_HOST_URL) -Dsonar.login=$(SONAR_LOGIN) -Dsonar.password=$(SONAR_PASS) endif - rm -rf sonar-scanner-2.8 sonar-scanner-2.8.zip + rm -rf sonar-scanner* diff --git a/README.md b/README.md index 238a519..74f2a7e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ -## Catchie +## node-catchie + [![CircleCI](https://circleci.com/gh/dial-once/node-catchie.svg?style=svg)](https://circleci.com/gh/dial-once/node-catchie) +[![Sonar](http://proxy.dialonce.net/sonar/api/badges/gate?key=node-node-catchie)](http://sonar.dialonce.net/dashboard?id=node-node-catchie) +[![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-node-catchie&metric=ncloc)](http://sonar.dialonce.net/dashboard?id=node-node-catchie) +[![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-node-catchie&metric=coverage)](http://sonar.dialonce.net/dashboard?id=node-node-catchie) +[![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-node-catchie&metric=code_smells)](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-node-catchie&metric=coverage) +[![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-node-catchie&metric=bugs)](http://sonar.dialonce.net/dashboard?id=node-node-catchie) +[![Sonar](http://proxy.dialonce.net/sonar/api/badges/measure?key=node-node-catchie&metric=sqale_debt_ratio)](http://sonar.dialonce.net/dashboard?id=node-node-catchie) + Repeats the provided function (sync and async) n times if an error occurs during its execution. @@ -24,7 +32,7 @@ By default, a __console__ will be used as a logger You can also silence the module and not let it log info about retries if you pass the boolean value: ```js // 2nd parameter is silence { boolean } -const catchie = require('catchie')(new winston.Logger(), true); +const catchie = require('catchie')(new winston.Logger(), true); ``` ## Usage diff --git a/package.json b/package.json index 4cc3c10..2dc486c 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "description": "Retries a provided function n times on error", "main": "src/index.js", "scripts": { - "test": "node_modules/.bin/eslint . && node_modules/.bin/_mocha --recursive test", "start": "node src/index.js", - "cover": "node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- test --recursive" + "lint": "./node_modules/.bin/eslint .", + "cover": "CONSOLE_LOGGING=false ./node_modules/.bin/nyc --reporter=lcov ./node_modules/.bin/_mocha test --recursive --timeout=10000 --exit", + "test": "npm run lint && npm run cover" }, "repository": { "type": "git", @@ -26,14 +27,16 @@ "url": "https://github.com/dial-once/node-catchie/issues" }, "homepage": "https://github.com/dial-once/node-catchie#readme", + "dependencies": { + "sinon": "^2.3.1" + }, "devDependencies": { - "eslint": "^3.19.0", - "eslint-config-airbnb": "^15.0.1", - "eslint-plugin-import": "^2.3.0", - "eslint-plugin-jsx-a11y": "^5.0.3", - "eslint-plugin-react": "^7.0.1", - "istanbul": "^0.4.5", + "eslint": "^5.4.0", + "eslint-config-airbnb": "^17.1.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-jsx-a11y": "^6.1.1", + "eslint-plugin-react": "^7.11.1", "mocha": "^5.2.0", - "sinon": "^2.3.1" + "nyc": "^12.0.2" } } diff --git a/sonar-project.properties b/sonar-project.properties index f7fd29b..d58c910 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,6 @@ sonar.projectName=node-catchie sonar.sources=src sonar.tests=test -sonar.language=js sonar.dynamicAnalysis=reuseReports sonar.javascript.jstest.reportsPath=coverage diff --git a/src/index.js b/src/index.js index 9af39f2..fd44af6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,21 @@ let defaultLogger = console; -/** - @class Catchie - Consume errors and retry the function execution given times +/* eslint-disable no-use-before-define */ - Supported environment variables: - CATCHIE_MAX_RETRY - cap amount of function retry -**/ +/** + * @class Catchie + * Consume errors and retry the function execution given times + * + * Supported environment variables: + * CATCHIE_MAX_RETRY - cap amount of function retry + * */ class Catchie { /** - @constructor - Construct instance of the module - @param logger {Object} - logger instnace - @param silent {boolean} - log or not the info about retries - **/ + * @constructor + * Construct instance of the module + * @param logger {Object} - logger instnace + * @param silent {boolean} - log or not the info about retries + * */ constructor(logger, silent) { this.logger = logger || defaultLogger; this.successCount = 0; @@ -22,39 +24,39 @@ class Catchie { } /** - @function clear - Reset the values of successCount and failureCount - **/ + * @function clear + * Reset the values of successCount and failureCount + * */ clear() { this.successCount = 0; this.failureCount = 0; } /** - @function callCount - Get the total amount of given function calls - @return {number} - total amount of function calls - **/ + * @function callCount + * Get the total amount of given function calls + * @return {number} - total amount of function calls + * */ get callCount() { return this.successCount + this.failureCount; } /** - @function retry - Execute the function given amount of times - @param fn { Function } - a function returning value / Promise - @retryCount { string|number } - amount of times to repeat a function on error - @return - { any } - if function is sync - { Promise } - if function is async - @throws if after the retryCount times of function execution the function still ended up with an error - @throws if fn param is not a function - **/ + * @function retry + * Execute the function given amount of times + * @param fn { Function } - a function returning value / Promise + * @retryCount { string|number } - amount of times to repeat a function on error + * @return + * { any } - if function is sync + * { Promise } - if function is async + * @throws if after the retryCount times of function execution the function still ended up with an error + * @throws if fn param is not a function + * */ retry(fn, retryCount = 1) { this.clear(); let timesToRetry = parseInt(retryCount || process.env.CATCHIE_MAX_RETRY, 10); - if (isNaN(timesToRetry)) { + if (Number.isNaN(timesToRetry)) { timesToRetry = 0; } @@ -73,7 +75,7 @@ class Catchie { if (!this.silent) { this.logger.info(`Retrying function due to ${e} error`); } - return repeatable(...args); // eslint-disable-line + return repeatable(...args); }; const repeatable = () => { @@ -84,21 +86,21 @@ class Catchie { if (value instanceof Promise) { // for promises a plain-old-catch does not work return Promise.resolve() - .then(() => value) - .then((result) => { - this.successCount ++; - return result; - }) - .catch((e) => { - this.failureCount ++; - return loopCheck(e, arguments); - }); + .then(() => value) + .then((result) => { + this.successCount++; + return result; + }) + .catch((e) => { + this.failureCount++; + return loopCheck(e, arguments); + }); } // if did not throw -> return either a Promise or an actual value - this.successCount ++; + this.successCount++; return value; } catch (e) { - this.failureCount ++; + this.failureCount++; return loopCheck(e, arguments); } }; @@ -108,11 +110,11 @@ class Catchie { } /** - @function - @param loggerInstance {object} - instance of a logger to use - @param silet {boolean} - log or not log the info about retries - @return {Object} - new instance of the module -**/ + * @function + * @param loggerInstance {object} - instance of a logger to use + * @param silet {boolean} - log or not log the info about retries + * @return {Object} - new instance of the module + * */ module.exports = (loggerInstance, silent = false) => { if (![null, undefined].includes(loggerInstance)) { defaultLogger = loggerInstance; diff --git a/test/catch-loop-spec.js b/test/catch-loop-spec.js index eb5372e..9899f72 100644 --- a/test/catch-loop-spec.js +++ b/test/catch-loop-spec.js @@ -13,7 +13,7 @@ describe('Repeater module tests', () => { it('Should retry the function on error', (done) => { let toThrow = true; - const functionToRepeat = function () { + const functionToRepeat = () => { if (toThrow) { toThrow = false; throw new Error(); @@ -66,7 +66,7 @@ describe('Repeater module tests', () => { it('Should not loose args passed to the function during retrys [no promise]', (done) => { let toThrow = true; - const functionToRepeat = function (arg1, arg2) { + const functionToRepeat = (arg1, arg2) => { if (toThrow) { toThrow = false; throw new Error(); @@ -102,17 +102,15 @@ describe('Repeater module tests', () => { it('Should not loose args passed to the function during retrys [promise]', () => { let toThrow = true; - const functionToRepeat = function (arg1, arg2) { - return new Promise((resolve, reject) => { - if (toThrow) { - toThrow = false; - reject(); - } - assert.equal(arg1, 'Hello'); - assert.equal(arg2, 'World'); - resolve(arg1 + arg2); - }); - }; + const functionToRepeat = (arg1, arg2) => (new Promise((resolve, reject) => { + if (toThrow) { + toThrow = false; + reject(); + } + assert.equal(arg1, 'Hello'); + assert.equal(arg2, 'World'); + resolve(arg1 + arg2); + })); const spy = sinon.spy(functionToRepeat.bind(this, 'Hello', 'World')); return catchie.retry(spy).then((result) => { assert.equal(spy.callCount, catchie.callCount); @@ -122,16 +120,15 @@ describe('Repeater module tests', () => { it('Should not loose args passed to the function during retrys [promise] [arrow function]', () => { let toThrow = true; - const functionToRepeat = (arg1, arg2) => - (new Promise((resolve, reject) => { - if (toThrow) { - toThrow = false; - reject(); - } - assert.equal(arg1, 'Hello'); - assert.equal(arg2, 'World'); - resolve(arg1 + arg2); - })); + const functionToRepeat = (arg1, arg2) => (new Promise((resolve, reject) => { + if (toThrow) { + toThrow = false; + reject(); + } + assert.equal(arg1, 'Hello'); + assert.equal(arg2, 'World'); + resolve(arg1 + arg2); + })); const spy = sinon.spy(functionToRepeat.bind(this, 'Hello', 'World')); return catchie.retry(spy).then((result) => { assert.equal(spy.callCount, catchie.callCount); @@ -140,10 +137,10 @@ describe('Repeater module tests', () => { }); it('Should propagate rejection after retries cap reached', () => { - const spy = sinon.spy(sinon.stub().returns(Promise.reject('Promise rejected'))); + const spy = sinon.spy(sinon.stub().returns(Promise.reject(new Error('Promise rejected')))); return catchie.retry(spy, 5).catch((e) => { assert(e instanceof Error); - assert.equal(e.message, 'Promise rejected'); + assert.equal(e, 'Error: Error: Promise rejected'); assert.equal(spy.callCount, catchie.callCount); }); }); @@ -240,29 +237,27 @@ describe('Repeater module tests', () => { it('should not loose context [promise] [rejected]', (done) => { const spy = sinon.spy(sinon.stub().returns(Promise.reject())); - const badFunction = () => - (new Promise((resolve) => { - setTimeout(() => resolve(spy()), 250); - })); + const badFunction = () => (new Promise((resolve) => { + setTimeout(() => resolve(spy()), 250); + })); catchie.retry(badFunction, 5) - .catch((e) => { - assert(e instanceof Error); - assert.equal(spy.callCount, catchie.callCount); - done(); - }); + .catch((e) => { + assert(e instanceof Error); + assert.equal(spy.callCount, catchie.callCount); + done(); + }); }); it('should not loose context [promise] [resolved]', (done) => { const spy = sinon.spy(sinon.stub().returns(Promise.resolve(15))); - const badFunction = () => - (new Promise((resolve) => { - setTimeout(() => resolve(spy()), 250); - })); + const badFunction = () => (new Promise((resolve) => { + setTimeout(() => resolve(spy()), 250); + })); catchie.retry(badFunction, 5) - .then((val) => { - assert.equal(val, 15); - assert.equal(spy.callCount, catchie.callCount); - done(); - }); + .then((val) => { + assert.equal(val, 15); + assert.equal(spy.callCount, catchie.callCount); + done(); + }); }); });