├── .circleci └── config.yml ├── .coverage.json ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── tests.yml ├── .gitignore ├── .travis.yml ├── .versions ├── LICENSE ├── README.md ├── assets ├── base.css ├── block-navigation.js ├── sort-arrow-sprite.png ├── sorter.js └── vendor │ ├── prettify.css │ └── prettify.js ├── changelog.md ├── client ├── client.instrumentation.tests.js ├── main.tests.js ├── methods.e2e.tests.js ├── methods.js └── methods.unit.tests.js ├── conf └── default-coverage.json ├── meteor-legacy-coverage ├── .gitignore ├── .versions ├── LICENSE ├── README.md ├── conf │ └── default-coverage.json ├── package.js ├── package.json └── server │ ├── boot.js │ ├── context │ ├── conf.js │ └── log.js │ ├── handlers.js │ ├── index.js │ ├── main.js │ ├── router.js │ └── services │ ├── core.js │ ├── coverage-data.js │ ├── instrumenter.js │ └── source-map.js ├── package.js ├── package.json ├── server ├── boot.js ├── context │ ├── conf.js │ └── log.js ├── handlers.js ├── index.js ├── main.js ├── report │ ├── report-common.js │ ├── report-coverage.js │ ├── report-generic.js │ ├── report-html.js │ ├── report-http.js │ ├── report-json-summary.js │ ├── report-remap.js │ ├── report-service.js │ ├── report-teamcity.js │ └── report-text-summary.js ├── router.js ├── services │ ├── core.js │ └── coverage-data.js └── tests.js └── someapp └── package.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:latest-browsers 6 | working_directory: ~/meteor-coverage 7 | steps: 8 | - checkout 9 | - run: npm install -g meteor 10 | - run: meteor npm install 11 | - run: mkdir .coverage 12 | - run: meteor npm test 13 | - run: meteor npm run lint || true # ignore eslint error 14 | -------------------------------------------------------------------------------- /.coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/packages/lmieulet_meteor-coverage.js" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | assets 2 | .npm 3 | .github 4 | conf 5 | test 6 | node_modules 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended"], 3 | "plugins": ["babel", "mocha", "import"], 4 | "parser": "babel-eslint", 5 | "env":{ 6 | "es6": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "no-useless-escape": [0], 14 | "consistent-return": [0], 15 | "eqeqeq": [2, "smart"], 16 | "import/no-extraneous-dependencies": [0], 17 | "mocha/no-exclusive-tests": "error", 18 | "no-console": [0], 19 | "no-debugger": [0], 20 | "no-unused-vars": [0], 21 | "no-undefined": [0], 22 | "no-undef": [0], 23 | "prefer-template": [0], 24 | "no-mixed-requires": [0], 25 | "quotes": [2, "single"], 26 | "radix": [0], 27 | "semi": [2, "always"], 28 | 29 | "camelcase": 0, 30 | "no-underscore-dangle": 0, 31 | 32 | "indent": [2, 2], 33 | "comma-dangle": [2, "never"], 34 | "no-dupe-args": 2, 35 | "no-dupe-keys": 2, 36 | "no-empty": 2, 37 | "no-extra-boolean-cast": 2, 38 | "no-extra-parens": 2, 39 | "no-extra-semi": 2, 40 | "no-func-assign": 2, 41 | "no-inner-declarations": 2, 42 | "no-irregular-whitespace": 2, 43 | "no-negated-in-lhs": 2, 44 | "no-sparse-arrays": 2, 45 | "no-unexpected-multiline": 2, 46 | "no-unreachable": 2, 47 | "use-isnan": 2, 48 | 49 | "block-scoped-var": 2, 50 | "curly": [2, "multi-line"], 51 | "default-case": 2, 52 | "guard-for-in": 0, 53 | "no-alert": 2, 54 | "no-caller": 2, 55 | "no-case-declarations": 2, 56 | "no-else-return": 2, 57 | "no-labels": 2, 58 | "no-empty-pattern": 2, 59 | "no-eq-null": 2, 60 | "no-eval": 2, 61 | 62 | "import/no-mutable-exports": 0, 63 | "import/no-unresolved": 0, 64 | "import/named": 2, 65 | "import/namespace": 2, 66 | "import/export": 2, 67 | "import/no-duplicates": 2 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | 7 | 8 | ## Current Behavior 9 | 10 | 11 | 12 | 13 | ## Possible Solution 14 | 15 | 16 | 17 | 18 | ## Steps to Reproduce (for bugs) 19 | 20 | 21 | 22 | 1. 23 | 2. 24 | 3. 25 | 4. 26 | 27 | ## Context 28 | 29 | 30 | 31 | 32 | ## Your Environment 33 | 34 | 35 | * Version used: 36 | * Environment name and version (e.g. Node 0.10.47): 37 | * Operating System and version: 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Motivation and Context 8 | 9 | 10 | 11 | 12 | ## How Has This Been Tested? 13 | 14 | 15 | 16 | 17 | 18 | ## Screenshots (if appropriate): 19 | 20 | ## Types of changes 21 | 22 | 23 | - [ ] Bug fix (non-breaking change which fixes an issue) 24 | - [ ] New feature (non-breaking change which adds functionality) 25 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 26 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # the test suite runs the tests (headless, server+client) for multiple Meteor releases 2 | name: Test suite 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Javascript standard lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: setup node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 20 21 | 22 | - name: cache dependencies 23 | uses: actions/cache@v3 24 | with: 25 | path: ~/.npm 26 | key: ${{ runner.os }}-node-20-${{ hashFiles('**/package-lock.json') }} 27 | restore-keys: | 28 | ${{ runner.os }}-node-20- 29 | 30 | - run: | 31 | npm ci 32 | npm run lint 33 | 34 | test: 35 | name: Meteor package tests 36 | # needs: [lint] 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | meteorRelease: 41 | - '3.0.1' 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v3 45 | 46 | - name: Install Node.js 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: 20 50 | 51 | - name: Setup meteor ${{ matrix.meteorRelease }} 52 | uses: meteorengineer/setup-meteor@v1 53 | with: 54 | meteor-release: ${{ matrix.meteorRelease }} 55 | 56 | - name: cache dependencies 57 | uses: actions/cache@v3 58 | with: 59 | path: ~/.npm 60 | key: ${{ runner.os }}-node-20-${{ hashFiles('**/package-lock.json') }} 61 | restore-keys: | 62 | ${{ runner.os }}-node-20- 63 | 64 | - name: Install test dependencies 65 | run: npm ci 66 | 67 | 68 | - name: Run tests 69 | run: meteor npm run test 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .npm 2 | .coverage 3 | packages 4 | node_modules 5 | local-packages.json 6 | .idea 7 | test 8 | someapp 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | 4 | addons: 5 | chrome: stable 6 | 7 | node_js: 8 | - "14" 9 | 10 | cache: 11 | directories: 12 | - $HOME/.meteor 13 | - $HOME/.npm 14 | 15 | before_cache: 16 | - rm -f $HOME/.meteor/log/*.log 17 | 18 | before_install: 19 | # Download Meteor - Keep in mind that you need 20 | # to remove your travis cache to get meteor updates 21 | - export PATH="$HOME/.meteor:$PATH:$(npm bin -g)" 22 | - npm install -g meteor 23 | 24 | # Install dependencies 25 | - npm install -g eslint coveralls codecov.io codacy-coverage 26 | - npm list -g 27 | - env 28 | - meteor-installer uninstall 29 | - meteor-installer install 30 | - meteor npm install 31 | 32 | services: 33 | - xvfb 34 | 35 | script: 36 | - ls "$HOME/.meteor" 37 | - meteor npm test 38 | - sed 's/packages\/meteor-coverage\///' someapp/.coverage/lcov.info | coveralls || true # ignore coveralls error 39 | - sed 's/packages\/meteor-coverage\///' someapp/.coverage/lcov.info | codecov || true # ignore codecov error 40 | - sed 's/packages\/meteor-coverage\///' someapp/.coverage/lcov.info | codacy-coverage || true # ignore codacy error 41 | - meteor npm run lint || true # ignore eslint error 42 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@7.2.3 2 | babel-runtime@1.3.0 3 | base64@1.0.11 4 | boilerplate-generator@1.6.0 5 | dynamic-import@0.5.1 6 | ecmascript@0.12.3 7 | ecmascript-runtime@0.7.0 8 | ecmascript-runtime-client@0.8.0 9 | ecmascript-runtime-server@0.7.1 10 | ejson@1.1.0 11 | fetch@0.1.0 12 | http@1.4.2 13 | inter-process-messaging@0.1.0 14 | lmieulet:meteor-coverage@3.0.0 15 | lmieulet:meteor-packages-coverage@0.2.0 16 | local-test:lmieulet:meteor-coverage@3.0.0 17 | logging@1.1.20 18 | meteor@1.9.2 19 | meteortesting:browser-tests@0.1.2 20 | meteortesting:mocha@0.4.4 21 | modern-browsers@0.1.3 22 | modules@0.13.0 23 | modules-runtime@0.10.3 24 | practicalmeteor:mocha-core@1.0.1 25 | promise@0.11.1 26 | routepolicy@1.1.0 27 | underscore@1.0.10 28 | url@1.2.0 29 | webapp@1.7.2 30 | webapp-hashing@1.0.9 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Leo Mieulet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meteor-coverage 2 | 3 | A meteor package that allows you to get the statement, line, function and branch coverage of Meteor project and package. 4 | 5 | This package uses the [istanbuljs](https://github.com/istanbuljs/istanbuljs) set of packages to generate reports. Starting from Meteor 1.8, this package does not instrument your code to get coverage as you can let babel do it using the [babel plugin istanbul](https://github.com/istanbuljs/babel-plugin-istanbul). 6 | 7 | It's a debug only package, so it does not affect your production build. 8 | 9 | ## CI Platforms supported 10 | 11 | | | Travis | Circle CI | Coveralls | Codecov | Codacy | 12 | | ------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 13 | | lmieulet:meteor-coverage | [![Build Status](https://app.travis-ci.com/serut/meteor-coverage.svg?branch=master)](https://app.travis-ci.com/serut/meteor-coverage) | [![Circle CI](https://circleci.com/gh/serut/meteor-coverage.svg?style=svg)](https://circleci.com/gh/serut/meteor-coverage) | [![Coverage Status](https://coveralls.io/repos/github/serut/meteor-coverage/badge.svg?branch=master)](https://coveralls.io/github/serut/meteor-coverage?branch=master) | [![codecov](https://codecov.io/gh/serut/meteor-coverage/branch/master/graph/badge.svg)](https://codecov.io/gh/serut/meteor-coverage) | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/481a0d40fbe742c6a62f3a87da367015)](https://www.codacy.com/gh/serut/meteor-coverage/dashboard) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/3679340dded44b84a44ca65862855216)](https://www.codacy.com/app/l-mieulet/meteor-coverage) | 14 | | [meteor-coverage-app-exemple](https://github.com/serut/meteor-coverage-app-exemple) | [![Build Status](https://travis-ci.org/serut/meteor-coverage-app-exemple.svg?branch=master)](https://travis-ci.org/serut/meteor-coverage-app-exemple) | [![Circle CI](https://circleci.com/gh/serut/meteor-coverage-app-exemple.svg?style=svg)](https://circleci.com/gh/serut/meteor-coverage-app-exemple) | [![Coverage Status](https://coveralls.io/repos/github/serut/meteor-coverage-app-exemple/badge.svg?branch=master)](https://coveralls.io/github/serut/meteor-coverage-app-exemple?branch=master) | [![codecov](https://codecov.io/gh/serut/meteor-coverage-app-exemple/branch/master/graph/badge.svg)](https://codecov.io/gh/serut/meteor-coverage-app-exemple) | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1a2997c614cf4da09452f47d70d72352)](https://www.codacy.com/app/l-mieulet/meteor-coverage-app-exemplee) | 15 | 16 | 17 | [![Dependency Status](https://img.shields.io/david/serut/meteor-coverage.svg)](https://david-dm.org/serut/meteor-coverage) 18 | [![devDependency Status](https://img.shields.io/david/dev/serut/meteor-coverage.svg)](https://david-dm.org/serut/meteor-coverage?type=dev) 19 | 20 | 21 | ## Compatibility 22 | 23 | | meteor-coverage | Meteor | spacejam & practicalmeteor | meteortesting:mocha | 24 | | ------------- |:----------:|:----------:|:----------:| 25 | | 1.x | <1.6.0 | ✔ | ✘ | 26 | | [not supported](https://github.com/meteor/meteor/issues/9281) | 1.6.0 <1.6.1 | ✘ | ✘ | 27 | | 2.x | >=1.6.1 and < 1.8| ✘ | ✔ | 28 | | 3.x | >=1.8| ✘ | ✔ | 29 | 30 | 31 | 32 | **Table of Contents** 33 | 34 | - [Installation](#installation) 35 | - [Specific setup for Meteor apps](#specific-setup-for-meteor-apps) 36 | - [Specific setup for Meteor package](#specific-setup-for-meteor-package) 37 | - [Specific setup for Typescript](#specific-setup-for-Typescript) 38 | - [Advanced setup for CI](#advanced-setup-for-ci) 39 | - [Coveralls](#coveralls) 40 | - [Codecov](#codecov) 41 | - [Global environment variable](#global-environment-variable) 42 | - [Config file](#config-file) 43 | - [My files are missing from my app coverage report](#my-files-are-missing-from-my-app-coverage-report) 44 | - [Meteor ignored folders and files](#meteor-ignored-folders-and-files) 45 | - [How to use another test runner](#how-to-use-another-test-runner) 46 | - [I want my reports referred to my original source files](#i-want-my-reports-referred-to-my-original-source-files) 47 | - [Client API](#client-api) 48 | - [Meteor.sendCoverage(callback)](#meteorsendcoveragecallback) 49 | - [Meteor.exportCoverage(type, callback)](#meteorexportcoveragetype-callback) 50 | - [Meteor.importCoverage(callback)](#meteorimportcoveragecallback) 51 | - [Contributing](#contributing) 52 | - [Credits](#credits) 53 | 54 | 55 | 56 | ## Installation 57 | 58 | ### Specific setup for Meteor apps 59 | 60 | Ensure you use at least Meteor version `v1.8`. If you are using Typescript, jump [here](#specific-setup-for-Typescript) 61 | 62 | Then, run the following : 63 | 64 | ```txt 65 | meteor add lmieulet:meteor-coverage meteortesting:mocha 66 | meteor npm init # If the package.json file does not exist 67 | meteor npm install --save-dev babel-plugin-istanbul 68 | ``` 69 | 70 | In order to instrument your code, you need to add the [`babel-plugin-istanbul`](https://github.com/istanbuljs/babel-plugin-istanbul) to your babel config. If you don't have a babel config file, edit your package.json file, or use [any other babel configuration file (see .babelrc.js)](https://babeljs.io/docs/en/config-files). 71 | 72 | ```json 73 | { 74 | "name": "my-package", 75 | "version": "1.0.0", 76 | "babel": { 77 | "env": { 78 | "COVERAGE": { 79 | "plugins": [ 80 | "istanbul" 81 | ] 82 | } 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | You must wrap the istanbul plugin with the `env` setting to disable the file-instrumentation of your project when you are not running the test coverage script. Just keep in mind that if you follow the here under script, babel will use the `istanbul` package only when `BABEL_ENV=COVERAGE`. 89 | 90 | Now, to run the coverage process, just add these new scripts inside your `package.json` in the root folder of your app: 91 | ```json 92 | "scripts": { 93 | "coverage:unit": "BABEL_ENV=COVERAGE TEST_BROWSER_DRIVER=puppeteer COVERAGE=1 COVERAGE_OUT_HTML=1 COVERAGE_APP_FOLDER=$PWD/ meteor test --once --driver-package meteortesting:mocha", 94 | "coverage:watch": "BABEL_ENV=COVERAGE COVERAGE=1 COVERAGE_VERBOSE=1 COVERAGE_APP_FOLDER=$PWD/ TEST_WATCH=1 meteor test --driver-package meteortesting:mocha" 95 | } 96 | ``` 97 | 98 | You can find more options on the [meteortesting readme](https://github.com/meteortesting/meteor-mocha#run-with-code-coverage). Let's try the watch mode : 99 | 100 | meteor npm run test:watch:coverage 101 | 102 | Now open your [browser test page localhost:3000/](http://localhost:3000/) and the page [localhost:3000/coverage](http://localhost:3000/coverage). You can notice the client coverage is completly missing but server one is there. A missing feature would be to save your client coverage with a widget. Instead, you need to enter this javascript in your browser console (in the page where tests are executed): 103 | 104 | Meteor.sendCoverage(function(stats,nbErr) {console.log(stats,nbErr);}); 105 | # Reopen localhost:3000/coverage to see that client coverage have been saved on server 106 | 107 | # Creates an html export inside coverage_app_folder/output_folder/index.html 108 | Meteor.exportCoverage("html", function(err) {console.log(err)}) 109 | 110 | Refresh the [localhost:3000/coverage](http://localhost:3000/coverage) in your browser to see there is client coverage now. 111 | 112 | ### Specific setup for Meteor package 113 | 114 | In a meteor package, you need to add inside the `package.js` file: 115 | 116 | ```js 117 | [...] 118 | Package.onTest(function (api) { 119 | api.use(['lmieulet:meteor-legacy-coverage@0.2.0', 'lmieulet:meteor-coverage@3.0.0','meteortesting:mocha']); 120 | [...] 121 | }); 122 | ``` 123 | 124 | Creating a Meteor package in 2018 is a nightmare, so please stay calm when you discover the following `package.json` that prevents so many issues : 125 | ``` 126 | "scripts": { 127 | "setup-test": "rm -rf ./someapp && meteor create --bare someapp && cd someapp && cp ../.coverage.json . && meteor npm i --save puppeteer && mkdir packages && ln -s ../../ ./packages/meteor-coverage", 128 | "test": "meteor npm run setup-test && cd someapp && TEST_BROWSER_DRIVER=puppeteer COVERAGE_VERBOSE=1 COVERAGE=1 COVERAGE_OUT_LCOVONLY=1 COVERAGE_APP_FOLDER=$(pwd)/ meteor test-packages --once --driver-package meteortesting:mocha ./packages/meteor-coverage", 129 | "test:watch": "cd someapp && TEST_WATCH=1 COVERAGE=1 COVERAGE_APP_FOLDER=$(pwd)/ meteor test-packages --driver-package meteortesting:mocha ./packages/meteor-coverage" 130 | } 131 | ``` 132 | The task `setup-test` is the cutting edge workaround that creates an empty meteor app that will run your test later. 133 | 134 | ### Specific setup for Typescript 135 | 136 | If you use Typescript, you cannot use babel to instrument your code, you need to rely on `lmieulet:meteor-legacy-coverage` (like packages). 137 | The installation is almost the same as normal Meteor application, but you don't need to install all the babel stuff, you just need to run the following : 138 | 139 | ```txt 140 | meteor add lmieulet:meteor-legacy-coverage 141 | ``` 142 | 143 | You can look at the [deskoh/Meteor-React-Typescript-Starter](https://github.com/deskoh/Meteor-React-Typescript-Starter) if you need to see a working exemple. 144 | 145 | ## Advanced setup for CI 146 | 147 | ### Coveralls 148 | 149 | Install 150 | 151 | meteor npm i --save-dev coveralls 152 | 153 | Add this after tests execution: 154 | 155 | # Send coverage report 156 | cat .coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js || true # ignore coveralls error 157 | 158 | ### Codecov 159 | 160 | Install 161 | 162 | meteor npm i --save-dev codecov.io 163 | 164 | Add this after tests execution: 165 | 166 | cat .coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js || true # ignore codecov error 167 | 168 | ## Global environment variable 169 | 170 | **Deprecated** 171 | You can provides settings by setting these environment variables: 172 | 173 | - `COVERAGE=1` to enable coverage 174 | - `COVERAGE_APP_FOLDER=/path/to/your/meteor/app/` 175 | - Used to see if you have a customized `.coverage.json` file 176 | - Used by istanbul in reports if the file has source map 177 | - Needs to end with a trailing slash 178 | - Used when importing or exporting coverage reports 179 | - `COVERAGE_VERBOSE=1` to see logs (optional) 180 | 181 | ## Config file 182 | 183 | If you have packages used by your project (ex: aldeed:simple-schema) or libraries on your client side (ex: OpenLayers, Jquery), you can hide the coverage of these files from reports. You can specify which files will not be covered in a `.coverage.json` file inside the `COVERAGE_APP_FOLDER` folder. 184 | 185 | If you do not have this file, this package will use the default one (`conf/default-coverage.json`). If you do not define a key in the `.coverage.json` file, the default one will be used. 186 | 187 | Exemple: 188 | 189 | ```json{ 190 | "--": "Meteor app does not require any specific configuration", 191 | "--": "If you want to instrument a package, you need to add the following", 192 | "remapFormat": ["html", "cobertura", "clover", "json", "json-summary", "lcovonly", "teamcity", "text", "text-summary"], 193 | "output": "./.coverage" 194 | } 195 | ``` 196 | 197 | Details : 198 | 199 | - The glob syntax can be found [here](http://www.linuxjournal.com/content/bash-extended-globbing). 200 | - To create your custom config file, run the project with `COVERAGE_VERBOSE=1` env variable and use logs to see which filenames were hooked or hidden. PR welcome. 201 | - The output folder needs to starts with a dot to exclude that folder from Meteor build. 202 | 203 | ## My files are missing from my app coverage report 204 | 205 | If you have **internal packages** inside your app and you want to get their **server side** coverage. Open the file `.meteor/packages` and move the line `lmieulet:meteor-coverage` to be above these packages. 206 | 207 | ## Meteor ignored folders and files 208 | 209 | - hidden folders like .npm, .coverage or .meteor. 210 | - special folders like node_modules. 211 | - all meteor packages (bundled and/or manually installed ones) like meteor/underscore, meteor/accounts-password or aldeed:simple-schema. 212 | - all tests file(s) containing `spec?|test?|specs?|tests?|app-specs?|app-tests?` and all folder(s) named `specs?|tests?|app-specs?|app-tests?` 213 | 214 | ## How to use another test runner 215 | 216 | You can find [here](https://github.com/practicalmeteor/spacejam/compare/windows-suppport...serut:windows-suppport-rc4?diff=split&name=windows-suppport-rc4#diff-f388d8f4ed9765929079f40166396fdeR65) the diff between "spacejam without coverage" and "spacejam coverage", so you can build something else, with grunt for example, that exports your test. meteortesting:mocha did also the same. 217 | 218 | ## I want my reports referred to my original source files 219 | 220 | If you are using a language that compiles to JavaScript (there are [lots of them](https://github.com/jashkenas/coffeescript/wiki/list-of-languages-that-compile-to-js)), you may want to see your coverage reports referred to the original source files (prior to compilation). 221 | 222 | To remap your source files, you have to provide the report type `out_remap` explicitly when using `spacejam`: `spacejam-mocha --coverage out_remap` 223 | 224 | You'll get your remapped coverage reports at `./.coverage/.remap` (or `custom_output/.remap` if you're customized the output folder through the file `.coverage.json`). 225 | 226 | The coverage is remapped to **all the available reports** (listed in the following example) by default. If you only want some of them, you need to request them explicitly through the key `remap.format` in `.coverage.json` like this: 227 | 228 | ```json 229 | { 230 | "remap": { 231 | "format": ["html", "clover", "cobertura", "json", "json-summary", "lcovonly", "teamcity", "text", "text-summary"] 232 | } 233 | } 234 | ``` 235 | 236 | If you want to remap the coverage with `Meteor.exportCoverage()`, then you must use the report type `remap`. 237 | 238 | This feature has only been tested with TypeScript, but it should work for any language compiled to JavaScript, just **make sure you generate source maps (\*.js.map) for all the compiled files and that source maps are located next to their respective compiled JavaScript file (\*.js)**, just like this: 239 | 240 | COVERAGE_APP_FOLDER 241 | ├── tsconfig.json 242 | ├── src 243 | | ├── my-file.ts 244 | | └── my-file.d.ts 245 | ├── build 246 | | ├── my-file.js 247 | | └── my-file.js.map 248 | 249 | * * * 250 | 251 | ## Client API 252 | 253 | #### Meteor.sendCoverage(callback) 254 | 255 | Run the following command in your browser and the client coverage will be saved into the server coverage report. 256 | 257 | ```js 258 | Meteor.sendCoverage(function(stats,nbErr) {console.log(stats,nbErr);}); 259 | ``` 260 | 261 | Why? When a browser opens the client side of your application, this package intercepts all queries matching `*.js` to respond the instrumented version of the original script, if they are not ignored by the configuration file. All these instrumented scripts are autonomous and they save the coverage in a global variable when you execute a line of a file. This global variable needs to be sent back to the server to create a full coverage report. 262 | 263 | #### Meteor.exportCoverage(type, callback) 264 | 265 | - type: the type of report you want to create inside your `COVERAGE_APP_FOLDER` 266 | 267 | - Default: `coverage`, used to dump the coverage object in a file because when there are several types of test, we want to merge results, and the server reloads between each one. 268 | - Allowed values: `coverage`, `html`, `json`, `json-summary`, `lcovonly`, `remap`, `text-summary` 269 | - **Not working values:** `clover`, `cobertura`, `lcov`, `teamcity`, `text`, `text-lcov`, PR welcome 270 | - Except for `coverage`, the file generation is handled by [istanbuljs/istanbul-reports](https://github.com/istanbuljs/istanbul-reports) 271 | 272 | ```js 273 | Meteor.exportCoverage(null, function(err) {console.log(err)}) 274 | ``` 275 | 276 | #### Meteor.importCoverage(callback) 277 | 278 | Import a `coverage` export. 279 | 280 | ```js 281 | Meteor.importCoverage(function(err) {console.log(err)}) 282 | ``` 283 | 284 | ## Contributing 285 | 286 | Anyone is welcome to contribute. 287 | 288 | # You should fork this repo first 289 | git clone https://github.com/serut/meteor-coverage 290 | # I have a preference to always execute npm action 291 | # throw the integrated meteor npm (npm --v !== meteor npm --v) 292 | meteor npm install 293 | # Edit the app_folder key to match your app folder, don't forget the ending slash 294 | nano settings.coverage.json 295 | # Then run mocha watch tests 296 | meteor npm run start 297 | 298 | ## Credits 299 | 300 | This package would not exist without the amazing work of: 301 | 302 | - [Contributors](https://github.com/serut/meteor-coverage/graphs/contributors) and testers for their help 303 | - [Xolv.io](http://xolv.io) and their work on the original [meteor-coverage](https://github.com/xolvio/meteor-coverage) package; 304 | - All contributors of [istanbuljs](https://github.com/istanbuljs/istanbuljs) project. 305 | 306 | All of them were very helpful in the development of this package. Merci ! 307 | -------------------------------------------------------------------------------- /assets/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | height: 100%; 4 | } 5 | body { 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | font-size: 14px; 8 | color:#333; 9 | } 10 | .small { font-size: 12px; } 11 | *, *:after, *:before { 12 | -webkit-box-sizing:border-box; 13 | -moz-box-sizing:border-box; 14 | box-sizing:border-box; 15 | } 16 | h1 { font-size: 20px; margin: 0;} 17 | h2 { font-size: 14px; } 18 | pre { 19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 | margin: 0; 21 | padding: 0; 22 | -moz-tab-size: 2; 23 | -o-tab-size: 2; 24 | tab-size: 2; 25 | } 26 | a { color:#0074D9; text-decoration:none; } 27 | a:hover { text-decoration:underline; } 28 | .strong { font-weight: bold; } 29 | .space-top1 { padding: 10px 0 0 0; } 30 | .pad2y { padding: 20px 0; } 31 | .pad1y { padding: 10px 0; } 32 | .pad2x { padding: 0 20px; } 33 | .pad2 { padding: 20px; } 34 | .pad1 { padding: 10px; } 35 | .space-left2 { padding-left:55px; } 36 | .space-right2 { padding-right:20px; } 37 | .center { text-align:center; } 38 | .clearfix { display:block; } 39 | .clearfix:after { 40 | content:''; 41 | display:block; 42 | height:0; 43 | clear:both; 44 | visibility:hidden; 45 | } 46 | .fl { float: left; } 47 | @media only screen and (max-width:640px) { 48 | .col3 { width:100%; max-width:100%; } 49 | .hide-mobile { display:none!important; } 50 | } 51 | 52 | .quiet { 53 | color: #7f7f7f; 54 | color: rgba(0,0,0,0.5); 55 | } 56 | .quiet a { opacity: 0.7; } 57 | 58 | .fraction { 59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 | font-size: 10px; 61 | color: #555; 62 | background: #E8E8E8; 63 | padding: 4px 5px; 64 | border-radius: 3px; 65 | vertical-align: middle; 66 | } 67 | 68 | div.path a:link, div.path a:visited { color: #333; } 69 | table.coverage { 70 | border-collapse: collapse; 71 | margin: 10px 0 0 0; 72 | padding: 0; 73 | } 74 | 75 | table.coverage td { 76 | margin: 0; 77 | padding: 0; 78 | vertical-align: top; 79 | } 80 | table.coverage td.line-count { 81 | text-align: right; 82 | padding: 0 5px 0 20px; 83 | } 84 | table.coverage td.line-coverage { 85 | text-align: right; 86 | padding-right: 10px; 87 | min-width:20px; 88 | } 89 | 90 | table.coverage td span.cline-any { 91 | display: inline-block; 92 | padding: 0 5px; 93 | width: 100%; 94 | } 95 | .missing-if-branch { 96 | display: inline-block; 97 | margin-right: 5px; 98 | border-radius: 3px; 99 | position: relative; 100 | padding: 0 4px; 101 | background: #333; 102 | color: yellow; 103 | } 104 | 105 | .skip-if-branch { 106 | display: none; 107 | margin-right: 10px; 108 | position: relative; 109 | padding: 0 4px; 110 | background: #ccc; 111 | color: white; 112 | } 113 | .missing-if-branch .typ, .skip-if-branch .typ { 114 | color: inherit !important; 115 | } 116 | .coverage-summary { 117 | border-collapse: collapse; 118 | width: 100%; 119 | } 120 | .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 | .keyline-all { border: 1px solid #ddd; } 122 | .coverage-summary td, .coverage-summary th { padding: 10px; } 123 | .coverage-summary tbody { border: 1px solid #bbb; } 124 | .coverage-summary td { border-right: 1px solid #bbb; } 125 | .coverage-summary td:last-child { border-right: none; } 126 | .coverage-summary th { 127 | text-align: left; 128 | font-weight: normal; 129 | white-space: nowrap; 130 | } 131 | .coverage-summary th.file { border-right: none !important; } 132 | .coverage-summary th.pct { } 133 | .coverage-summary th.pic, 134 | .coverage-summary th.abs, 135 | .coverage-summary td.pct, 136 | .coverage-summary td.abs { text-align: right; } 137 | .coverage-summary td.file { white-space: nowrap; } 138 | .coverage-summary td.pic { min-width: 120px !important; } 139 | .coverage-summary tfoot td { } 140 | 141 | .coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | .coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | .coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | .status-line { height: 10px; } 155 | /* yellow */ 156 | .cbranch-no { background: yellow !important; color: #111; } 157 | /* dark red */ 158 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 159 | .low .chart { border:1px solid #C21F39 } 160 | .highlighted, 161 | .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ 162 | background: #C21F39 !important; 163 | } 164 | /* medium red */ 165 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 166 | /* light red */ 167 | .low, .cline-no { background:#FCE1E5 } 168 | /* light green */ 169 | .high, .cline-yes { background:rgb(230,245,208) } 170 | /* medium green */ 171 | .cstat-yes { background:rgb(161,215,106) } 172 | /* dark green */ 173 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 174 | .high .chart { border:1px solid rgb(77,146,33) } 175 | 176 | .medium .chart { border:1px solid #666; } 177 | .medium .cover-fill { background: #666; } 178 | 179 | .cstat-skip { background: #ddd; color: #111; } 180 | .fstat-skip { background: #ddd; color: #111 !important; } 181 | .cbranch-skip { background: #ddd !important; color: #111; } 182 | 183 | span.cline-neutral { background: #eaeaea; } 184 | .medium { background: #eaeaea; } 185 | 186 | .coverage-summary td.empty { 187 | opacity: .5; 188 | padding-top: 4px; 189 | padding-bottom: 4px; 190 | line-height: 1; 191 | color: #888; 192 | } 193 | 194 | .cover-fill, .cover-empty { 195 | display:inline-block; 196 | height: 12px; 197 | } 198 | .chart { 199 | line-height: 0; 200 | } 201 | .cover-empty { 202 | background: white; 203 | } 204 | .cover-full { 205 | border-right: none !important; 206 | } 207 | pre.prettyprint { 208 | border: none !important; 209 | padding: 0 !important; 210 | margin: 0 !important; 211 | } 212 | .com { color: #999 !important; } 213 | .ignore-none { color: #999; font-weight: normal; } 214 | 215 | .wrapper { 216 | min-height: 100%; 217 | height: auto !important; 218 | height: 100%; 219 | margin: 0 auto -48px; 220 | } 221 | .footer, .push { 222 | height: 48px; 223 | } 224 | -------------------------------------------------------------------------------- /assets/block-navigation.js: -------------------------------------------------------------------------------- 1 | var jumpToCode = (function init () { 2 | // Classes of code we would like to highlight 3 | var missingCoverageClasses = [ '.cbranch-no', '.cstat-no', '.fstat-no' ]; 4 | 5 | // We don't want to select elements that are direct descendants of another match 6 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 7 | 8 | // Selecter that finds elements on the page to which we can jump 9 | var selector = notSelector + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 10 | 11 | // The NodeList of matching elements 12 | var missingCoverageElements = document.querySelectorAll(selector); 13 | 14 | var currentIndex; 15 | 16 | function toggleClass(index) { 17 | missingCoverageElements.item(currentIndex).classList.remove('highlighted'); 18 | missingCoverageElements.item(index).classList.add('highlighted'); 19 | } 20 | 21 | function makeCurrent(index) { 22 | toggleClass(index); 23 | currentIndex = index; 24 | missingCoverageElements.item(index) 25 | .scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); 26 | } 27 | 28 | function goToPrevious() { 29 | var nextIndex = 0; 30 | if (typeof currentIndex !== 'number' || currentIndex === 0) { 31 | nextIndex = missingCoverageElements.length - 1; 32 | } else if (missingCoverageElements.length > 1) { 33 | nextIndex = currentIndex - 1; 34 | } 35 | 36 | makeCurrent(nextIndex); 37 | } 38 | 39 | function goToNext() { 40 | var nextIndex = 0; 41 | 42 | if (typeof currentIndex === 'number' && currentIndex < (missingCoverageElements.length - 1)) { 43 | nextIndex = currentIndex + 1; 44 | } 45 | 46 | makeCurrent(nextIndex); 47 | } 48 | 49 | return function jump(event) { 50 | switch (event.which) { 51 | case 78: // n 52 | case 74: // j 53 | goToNext(); 54 | break; 55 | case 66: // b 56 | case 75: // k 57 | case 80: // p 58 | goToPrevious(); 59 | break; 60 | } 61 | }; 62 | }()); 63 | window.addEventListener('keydown', jumpToCode); 64 | -------------------------------------------------------------------------------- /assets/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serut/meteor-coverage/050d739f7c77279f7dc101ac6bb1dc45aab9f724/assets/sort-arrow-sprite.png -------------------------------------------------------------------------------- /assets/sorter.js: -------------------------------------------------------------------------------- 1 | var addSorting = (function () { 2 | "use strict"; 3 | var cols, 4 | currentSort = { 5 | index: 0, 6 | desc: false 7 | }; 8 | 9 | // returns the summary table element 10 | function getTable() { return document.querySelector('.coverage-summary'); } 11 | // returns the thead element of the summary table 12 | function getTableHeader() { return getTable().querySelector('thead tr'); } 13 | // returns the tbody element of the summary table 14 | function getTableBody() { return getTable().querySelector('tbody'); } 15 | // returns the th element for nth column 16 | function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; } 17 | 18 | // loads all columns 19 | function loadColumns() { 20 | var colNodes = getTableHeader().querySelectorAll('th'), 21 | colNode, 22 | cols = [], 23 | col, 24 | i; 25 | 26 | for (i = 0; i < colNodes.length; i += 1) { 27 | colNode = colNodes[i]; 28 | col = { 29 | key: colNode.getAttribute('data-col'), 30 | sortable: !colNode.getAttribute('data-nosort'), 31 | type: colNode.getAttribute('data-type') || 'string' 32 | }; 33 | cols.push(col); 34 | if (col.sortable) { 35 | col.defaultDescSort = col.type === 'number'; 36 | colNode.innerHTML = colNode.innerHTML + ''; 37 | } 38 | } 39 | return cols; 40 | } 41 | // attaches a data attribute to every tr element with an object 42 | // of data values keyed by column name 43 | function loadRowData(tableRow) { 44 | var tableCols = tableRow.querySelectorAll('td'), 45 | colNode, 46 | col, 47 | data = {}, 48 | i, 49 | val; 50 | for (i = 0; i < tableCols.length; i += 1) { 51 | colNode = tableCols[i]; 52 | col = cols[i]; 53 | val = colNode.getAttribute('data-value'); 54 | if (col.type === 'number') { 55 | val = Number(val); 56 | } 57 | data[col.key] = val; 58 | } 59 | return data; 60 | } 61 | // loads all row data 62 | function loadData() { 63 | var rows = getTableBody().querySelectorAll('tr'), 64 | i; 65 | 66 | for (i = 0; i < rows.length; i += 1) { 67 | rows[i].data = loadRowData(rows[i]); 68 | } 69 | } 70 | // sorts the table using the data for the ith column 71 | function sortByIndex(index, desc) { 72 | var key = cols[index].key, 73 | sorter = function (a, b) { 74 | a = a.data[key]; 75 | b = b.data[key]; 76 | return a < b ? -1 : a > b ? 1 : 0; 77 | }, 78 | finalSorter = sorter, 79 | tableBody = document.querySelector('.coverage-summary tbody'), 80 | rowNodes = tableBody.querySelectorAll('tr'), 81 | rows = [], 82 | i; 83 | 84 | if (desc) { 85 | finalSorter = function (a, b) { 86 | return -1 * sorter(a, b); 87 | }; 88 | } 89 | 90 | for (i = 0; i < rowNodes.length; i += 1) { 91 | rows.push(rowNodes[i]); 92 | tableBody.removeChild(rowNodes[i]); 93 | } 94 | 95 | rows.sort(finalSorter); 96 | 97 | for (i = 0; i < rows.length; i += 1) { 98 | tableBody.appendChild(rows[i]); 99 | } 100 | } 101 | // removes sort indicators for current column being sorted 102 | function removeSortIndicators() { 103 | var col = getNthColumn(currentSort.index), 104 | cls = col.className; 105 | 106 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 107 | col.className = cls; 108 | } 109 | // adds sort indicators for current column being sorted 110 | function addSortIndicators() { 111 | getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted'; 112 | } 113 | // adds event listeners for all sorter widgets 114 | function enableUI() { 115 | var i, 116 | el, 117 | ithSorter = function ithSorter(i) { 118 | var col = cols[i]; 119 | 120 | return function () { 121 | var desc = col.defaultDescSort; 122 | 123 | if (currentSort.index === i) { 124 | desc = !currentSort.desc; 125 | } 126 | sortByIndex(i, desc); 127 | removeSortIndicators(); 128 | currentSort.index = i; 129 | currentSort.desc = desc; 130 | addSortIndicators(); 131 | }; 132 | }; 133 | for (i =0 ; i < cols.length; i += 1) { 134 | if (cols[i].sortable) { 135 | // add the click event handler on the th so users 136 | // dont have to click on those tiny arrows 137 | el = getNthColumn(i).querySelector('.sorter').parentElement; 138 | if (el.addEventListener) { 139 | el.addEventListener('click', ithSorter(i)); 140 | } else { 141 | el.attachEvent('onclick', ithSorter(i)); 142 | } 143 | } 144 | } 145 | } 146 | // adds sorting functionality to the UI 147 | return function () { 148 | if (!getTable()) { 149 | return; 150 | } 151 | cols = loadColumns(); 152 | loadData(cols); 153 | addSortIndicators(); 154 | enableUI(); 155 | }; 156 | })(); 157 | 158 | window.addEventListener('load', addSorting); 159 | -------------------------------------------------------------------------------- /assets/vendor/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /assets/vendor/prettify.js: -------------------------------------------------------------------------------- 1 | window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); 2 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | #### 3.2.0 2 | - Handle Typescript Meteor projects 3 | 4 | #### 3.1.1 5 | - Fix HTML and HTTP reports type 6 | 7 | #### 3.0.0 8 | Move to babel instrumenter for meteor apps. 9 | 10 | Breaking changes: 11 | This package does not make file coverage anymore, you need to add a third plugin to do that. 12 | 13 | For meteor apps, use the babel module named istanbul. 14 | For packages, you need to add lmieulet:meteor-packages-coverage. That's a copy of meteor-coverage some commits ago. So it handles the instrumentation like before. 15 | 16 | #### 2.0.2 17 | Fix Meteor.sendCoverage function, otherwise test runner thinks they failed to import client coverage. 18 | 19 | #### 2.0.1 20 | Republishing... 21 | 22 | #### 2.0.0 23 | - Migrates to Meteor 1.6. 24 | For Meteor 1.4 apps please use meteor-coverage@1.1.4. 25 | - remove some outdated libraries in favor to new ones: 26 | - meteorhacks:picker -> webapp (directly from meteor) 27 | - Jquery -> http (directly from meteor) 28 | - istanbul-api -> istanbuljs/istanbuljs 29 | 30 | #### 1.1.4 31 | 32 | #### 1.0.1 33 | - map a lot better the paths inside `.map` files to real paths. You can now see .npm dependencies for exemple. 34 | - improved glob patterns in `.coverage.json`: previously, all files were covered. Now **a lot less** are covered. To be able to cover packages, you need to specify it on the `.coverage.json` 35 | - add warning when `.coverage.json` is an invalid JSON 36 | - environment variables are now deprecated in favor of meteor `--settings` 37 | - on boot, meteor-coverage creates the report folder 38 | - fix verbosity ignored on report generation 39 | - add new type of export: `remap`. It adds support to languages that compile to JavaScript, like TypeScript 40 | 41 | #### 0.9.4 42 | 43 | - The configuration file `.coverage.json` is now written in minimatch syntax instead of regex 44 | Thanks to @thiagodelgado111 for his pull request #13 45 | - add new type of export: `text-summary` 46 | - add Meteor.settings support (can replace environment variable) 47 | - lint, tests, and more es6 48 | 49 | #### 0.9.1 50 | 51 | - fix a bug that prevents an app to boot when meteor-coverage is present but not covering (COVERAGE=0) 52 | 53 | #### 0.9.0 54 | 55 | - move code structure to es6 56 | - remove tinytest and use mocha instead 57 | - add a new option in the `coverage.json` file to set up the output folder, default is `./.coverage` 58 | - add new type of export: `html`, `json`, `json_summary` 59 | - spacejam received an update 60 | 61 | [Meteor forum thread](https://forums.meteor.com/t/coverage-on-meteor/20035) 62 | -------------------------------------------------------------------------------- /client/client.instrumentation.tests.js: -------------------------------------------------------------------------------- 1 | import { HTTP } from 'meteor/http'; 2 | 3 | describe('meteor-coverage', function (done) { 4 | 5 | it('download a lot of times a covered js file, ensure it"s not corrumpted', function (done) { 6 | this.timeout(0); 7 | 8 | let i = 0; 9 | const downloadPageAndCheckLength = function (i) { 10 | // meteor-coverage package is a ES6 package 11 | HTTP.get('/packages/lmieulet_meteor-coverage.js', {}, (error, response) => { 12 | if (response.content.length > 5400) { 13 | if (i < 10) { 14 | return downloadPageAndCheckLength(i+1); 15 | } 16 | return done(); 17 | } 18 | return done(response.content.length, 'Request returned as corrumpted'); 19 | }); 20 | }; 21 | downloadPageAndCheckLength(i); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/main.tests.js: -------------------------------------------------------------------------------- 1 | import './methods.unit.tests'; 2 | import './methods.e2e.tests'; 3 | import './client.instrumentation.tests'; 4 | -------------------------------------------------------------------------------- /client/methods.e2e.tests.js: -------------------------------------------------------------------------------- 1 | import Meteor from 'meteor/lmieulet:meteor-coverage'; 2 | import chai from 'chai'; 3 | const {assert, expect, should} = chai; 4 | var _should = should(); 5 | 6 | var testCoverage = function(done, operation, reportType) { 7 | this.timeout(0); 8 | try { 9 | let params = operation === 'export' ? [reportType] : []; 10 | params.push(function (err) { 11 | assert.isUndefined(err); 12 | done(); 13 | }); 14 | Meteor[`${operation}Coverage`].apply(this, params); 15 | } catch (e) { 16 | console.error(e, e.stack); 17 | done(e); 18 | } 19 | }; 20 | 21 | describe('meteor-coverage', function () { 22 | 23 | it('send client coverage', function (done) { 24 | this.timeout(10000); 25 | try { 26 | Meteor.sendCoverage( 27 | function (stats, err) { 28 | assert.isTrue(stats.TOTAL > 0, 'no client coverage'); 29 | assert.isTrue(stats.SUCCESS > 0, 'none of the client coverage have been saved'); 30 | assert.isTrue(stats.FAILED === 0, 'an export failed'); 31 | done(); 32 | } 33 | ); 34 | } catch (e) { 35 | console.error(e, e.stack); 36 | done(e); 37 | } 38 | }); 39 | 40 | // test implemented report types 41 | let coverage = { 42 | export: ['coverage', 'html', 'json', 'json-summary', 'lcovonly', 'remap', 'text-summary'], 43 | import: ['coverage'] 44 | }; 45 | for (let operation in coverage) { 46 | if (coverage.hasOwnProperty(operation)) { 47 | coverage[operation].forEach(function (reportType) { 48 | it(`${operation} [${reportType}]`, function (done) { 49 | testCoverage.call(this, done, operation, reportType); // pass mocha context 50 | }); 51 | }); 52 | } 53 | } 54 | 55 | // test non-implemented report types 56 | let reportTypes = { 57 | disallowed: ['', 'none', 'random-invented'], 58 | pending: ['clover', 'cobertura', 'lcov', 'text', 'text-lcov'] 59 | }; 60 | for (let group in reportTypes) { 61 | if (reportTypes.hasOwnProperty(group)) { 62 | reportTypes[group].forEach(function (reportType) { 63 | it(`export [${reportType}] should fail`, function (done) { 64 | this.timeout(0); 65 | try { 66 | Meteor.exportCoverage(reportType, function (err) { 67 | if (group === 'disallowed') { 68 | expect(err).to.be.undefined; 69 | } else { 70 | err.should.be.a('string'); 71 | assert.isTrue(err.startsWith('Error: ')); 72 | } 73 | done(); 74 | }); 75 | } catch (e) { 76 | console.error(e, e.stack); 77 | done(e); 78 | } 79 | }); 80 | }); 81 | } 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /client/methods.js: -------------------------------------------------------------------------------- 1 | import { HTTP } from 'meteor/http'; 2 | import { Meteor } from 'meteor/meteor'; 3 | 4 | var stats = {SUCCESS: 0, FAILED: 0, TOTAL: 0}; 5 | Meteor.getStats = function () { 6 | return stats; 7 | }; 8 | 9 | Meteor.increaseSuccess = function () { 10 | stats.SUCCESS++; 11 | }; 12 | 13 | Meteor.increaseFailures = function () { 14 | stats.FAILED++; 15 | }; 16 | 17 | Meteor.increaseTotal = function () { 18 | stats.TOTAL++; 19 | }; 20 | 21 | Meteor.getCoverageObject = function () { 22 | return global['__coverage__']; 23 | }; 24 | 25 | Meteor.getCoverageReportObject = function (propertyKey, value) { 26 | var coverageReport = {}; 27 | coverageReport[propertyKey] = value; 28 | 29 | return JSON.stringify(coverageReport); 30 | }; 31 | 32 | /** 33 | * Usage: Meteor.sendCoverage(function(stats,err) {console.log(stats,err);}); 34 | */ 35 | Meteor.sendCoverage = function (callback) { 36 | var coverageReport = {}; 37 | 38 | var globalCoverage = Meteor.getCoverageObject(); 39 | if (!globalCoverage) { 40 | return callback(Meteor.getStats()); 41 | } 42 | 43 | // Send each property alone 44 | for (var property in globalCoverage) { 45 | /* istanbul ignore else */ 46 | if (globalCoverage.hasOwnProperty(property)) { 47 | Meteor.increaseTotal(); 48 | 49 | HTTP.call('POST', '/coverage/client', { 50 | content: Meteor.getCoverageReportObject(property, globalCoverage[property]), 51 | headers: { 52 | 'Content-Type': 'application/json; charset=UTF-8' 53 | } 54 | }, (error, res) => { 55 | if (error) { 56 | Meteor.increaseFailures(); 57 | } else { 58 | Meteor.increaseSuccess(); 59 | } 60 | 61 | var stats = Meteor.getStats(); 62 | /* istanbul ignore else */ 63 | if (stats.SUCCESS + stats.FAILED === stats.TOTAL) { 64 | if (stats.FAILED > 0) { 65 | // This is bullshit. Should not be done like that 66 | // Test runners test if the second params is a truth value, so let's use a number 67 | return callback(stats, stats.FAILED); 68 | } 69 | return callback(stats); 70 | } 71 | }); 72 | } 73 | } 74 | }; 75 | /** 76 | * Usage: Meteor.exportCoverage(null, function(err) {console.log(err)}) 77 | */ 78 | Meteor.exportCoverage = function (type, callback) { 79 | /* istanbul ignore next: ternary operator */ 80 | var url = type ? '/coverage/export/'+type : '/coverage/export'; 81 | HTTP.call('GET', url, {}, (error, res) => { 82 | if (error) { 83 | return callback('Error: '+JSON.stringify(arguments)+'. A server error occurred while trying to export coverage data'); 84 | } 85 | 86 | try { 87 | let result = JSON.parse(res.content); 88 | /* istanbul ignore else */ 89 | if (result.type !== 'success') { 90 | throw new Error('Error: '+JSON.stringify(arguments)+'. An unexpected error occurred while trying to export coverage data'); 91 | } 92 | 93 | return callback(); 94 | } catch (e) { 95 | return callback(e); 96 | } 97 | }); 98 | }; 99 | 100 | /** 101 | * Usage: Meteor.importCoverage(function(err) {console.log(err)}) 102 | */ 103 | Meteor.importCoverage = function (callback) { 104 | HTTP.call('GET', '/coverage/import', {}, (error, res) => { 105 | if (error) { 106 | return callback(error, [error]); 107 | } 108 | 109 | try { 110 | let result = JSON.parse(res.content); 111 | /* istanbul ignore else */ 112 | if (result.type !== 'success') { 113 | throw new Error('Error: '+JSON.stringify(arguments)+'. An unexpected error occurred while trying to import coverage data'); 114 | } 115 | 116 | return callback(); 117 | } catch (e) { 118 | callback(e, [res]); 119 | } 120 | }); 121 | }; 122 | 123 | export default Meteor; 124 | -------------------------------------------------------------------------------- /client/methods.unit.tests.js: -------------------------------------------------------------------------------- 1 | import { HTTP } from 'meteor/http'; 2 | import chai from 'chai'; 3 | import sinon from 'sinon'; 4 | import sinonChai from 'sinon-chai'; 5 | import Meteor from 'meteor/lmieulet:meteor-coverage'; 6 | const {expect, assert} = chai; 7 | chai.use(sinonChai); 8 | 9 | describe('meteor-coverage', function (done) { 10 | 11 | let sandbox; 12 | beforeEach(function () { 13 | sandbox = sinon.createSandbox(); 14 | }); 15 | 16 | afterEach(function () { 17 | sandbox.restore(); 18 | }); 19 | 20 | it('should be defined', function () { 21 | assert.isDefined(Meteor.exportCoverage); 22 | assert.isDefined(Meteor.getCoverageObject); 23 | assert.isDefined(Meteor.importCoverage); 24 | assert.isDefined(Meteor.sendCoverage); 25 | }); 26 | 27 | it('export coverage', function () { 28 | const callback = sandbox.spy(); 29 | 30 | sandbox.stub(JSON, 'parse').returns({type: 'success'}); 31 | sandbox.stub(HTTP, 'call').callsFake(function(verb, url, config, c) { 32 | c(); 33 | }); 34 | 35 | Meteor.exportCoverage('test', callback); 36 | expect(callback).to.have.been.called; 37 | }); 38 | 39 | it('import coverage', function () { 40 | const callback = sandbox.spy(); 41 | 42 | sandbox.stub(JSON, 'parse').returns({type: 'success'}); 43 | sandbox.stub(HTTP, 'call').callsFake(function(verb, url, config, c) { 44 | c(); 45 | }); 46 | 47 | Meteor.importCoverage(callback); 48 | expect(callback).to.have.been.called; 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /conf/default-coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "remapFormat": ["html", "cobertura", "clover", "json", "json-summary", "lcovonly", "teamcity", "text", "text-summary"], 3 | "output": "./.coverage" 4 | } 5 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/.gitignore: -------------------------------------------------------------------------------- 1 | .npm 2 | .coverage 3 | packages 4 | node_modules 5 | local-packages.json 6 | .idea 7 | test 8 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@7.10.4 2 | babel-runtime@1.5.1 3 | base64@1.0.12 4 | boilerplate-generator@1.7.1 5 | callback-hook@1.5.1 6 | dynamic-import@0.7.3 7 | ecmascript@0.16.7 8 | ecmascript-runtime@0.8.1 9 | ecmascript-runtime-client@0.12.1 10 | ecmascript-runtime-server@0.11.0 11 | ejson@1.1.3 12 | fetch@0.1.3 13 | inter-process-messaging@0.1.1 14 | lmieulet:meteor-legacy-coverage@0.2.0 15 | logging@1.3.2 16 | meteor@1.11.2 17 | modern-browsers@0.1.9 18 | modules@0.19.0 19 | modules-runtime@0.13.1 20 | promise@0.12.2 21 | react-fast-refresh@0.2.7 22 | routepolicy@1.1.1 23 | underscore@1.0.13 24 | webapp@1.13.5 25 | webapp-hashing@1.1.1 26 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Leo Mieulet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/README.md: -------------------------------------------------------------------------------- 1 | # meteor-packages-coverage 2 | 3 | Fork of meteor-coverage that uses the [istanbuljs](https://github.com/istanbuljs/istanbuljs) package for code coverage. 4 | 5 | We use this package for meteor packages instrumentation because we can't use the babel plugin istanbul while testing a meteor package. -------------------------------------------------------------------------------- /meteor-legacy-coverage/conf/default-coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | ], 4 | "exclude": { 5 | "general": [], 6 | "server": [ 7 | "**/node_modules/**/*.json", 8 | "**/.!(meteor)*/**", 9 | "**/packages/!(local-test_?*.js)", 10 | "**/+([^:]):+([^:])/**", 11 | "**/@(test|tests|spec|specs)/**", 12 | "**/?(*.)test?(s).?*", 13 | "**/?(*.)spec?(s).?*", 14 | "**/?(*.)app-test?(s).?*", 15 | "**/?(*.)app-spec?(s).?*" 16 | ], 17 | "client": [ 18 | "**/*.json", 19 | "**/client/stylesheets/**", 20 | "**/.npm/package/node_modules/**", 21 | "**/web.browser/packages/**", 22 | "**/.?*/**", 23 | "**/packages/!(local-test_?*.js)", 24 | "**/+([^:]):+([^:])/**", 25 | "**/@(test|tests|spec|specs)/**", 26 | "**/?(*.)test?(s).?*", 27 | "**/?(*.)spec?(s).?*", 28 | "**/?(*.)app-test?(s).?*", 29 | "**/?(*.)app-spec?(s).?*" 30 | ] 31 | }, 32 | "remapFormat": ["html", "cobertura", "clover", "json", "json-summary", "lcovonly", "teamcity", "text", "text-summary"], 33 | "output": "./.coverage" 34 | } 35 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'lmieulet:meteor-legacy-coverage', 3 | version: '0.2.0', 4 | summary: 'Instrument packages and app files in a legacy way', 5 | git: 'https://github.com/serut/meteor-coverage', 6 | documentation: 'README.md', 7 | debugOnly: true // this package is not included on prod 8 | }); 9 | 10 | const dependencies = { 11 | 'istanbul-lib-source-maps': '2.0.1', 12 | 'istanbul-lib-instrument': '3.3.0', 13 | 'istanbul-lib-hook': '2.0.1', 14 | 'istanbul-lib-coverage': '2.0.1', 15 | 'istanbul-lib-report': '2.0.2', 16 | 'istanbul-reports': '2.0.1', 17 | 'body-parser': '1.18.2', 18 | 'minimatch': '3.0.4', 19 | 'mkdirp': '0.5.1', 20 | 'homedir': '0.6.0', 21 | 'remap-istanbul': '0.6.4' 22 | }; 23 | 24 | Package.onUse(function (api) { 25 | api.versionsFrom('METEOR@1.6.1'); 26 | 27 | api.use(['ecmascript']); 28 | api.use('webapp', 'server'); 29 | // Add datasets 30 | api.addAssets('conf/default-coverage.json', 'server'); 31 | 32 | api.mainModule('server/index.js', 'server'); 33 | Npm.depends(dependencies); 34 | }); 35 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-legacy-coverage", 3 | "version": "0.2.0", 4 | "description": "Instrument packages files", 5 | "main": "server/index.js", 6 | "dependencies": { 7 | "body-parser": "1.18.2", 8 | "homedir": "0.6.0", 9 | "istanbul-lib-coverage": "^2.0.1", 10 | "istanbul-lib-hook": "^2.0.1", 11 | "istanbul-lib-instrument": "^3.0.0", 12 | "istanbul-lib-report": "^2.0.2", 13 | "istanbul-lib-source-maps": "^2.0.1", 14 | "istanbul-reports": "^2.0.1", 15 | "minimatch": "3.0.4", 16 | "mkdirp": "0.5.1", 17 | "remap-istanbul": "0.6.4" 18 | }, 19 | "devDependencies": { 20 | "babel-cli": "^6.11.4", 21 | "babel-preset-es2015": "^6.13.2", 22 | "babel-preset-stage-0": "^6.5.0", 23 | "babel-register": "^6.11.6" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/serut/meteor-coverage.git" 28 | }, 29 | "author": "Léo Mieulet", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/serut/meteor-coverage/issues" 33 | }, 34 | "homepage": "https://github.com/serut/meteor-coverage#readme" 35 | } 36 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/boot.js: -------------------------------------------------------------------------------- 1 | import Router from './router'; 2 | import Instrumenter from './services/instrumenter'; 3 | import SourceMap from './services/source-map'; 4 | 5 | export default Boot = { 6 | startup() { 7 | // Search for PUTs and check whether called from inside/outside a PUT dir 8 | SourceMap.initialSetup(); 9 | // Start to collect coverage 10 | Instrumenter.hookLoader(); 11 | // Connect the router to this app 12 | new Router(); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/context/conf.js: -------------------------------------------------------------------------------- 1 | import Log from './log'; 2 | const meteor_parameters = { 3 | // /:\ ES 6 4 | // return the value OR UNDEFINED 5 | // THIS IS NOT A BOOLEAN 6 | IS_COVERAGE_ACTIVE: Meteor && Meteor.settings && Meteor.settings.coverage && Meteor.settings.coverage.is_coverage_active, 7 | COVERAGE_APP_FOLDER: Meteor && Meteor.settings && Meteor.settings.coverage && Meteor.settings.coverage.coverage_app_folder 8 | }; 9 | 10 | export const IS_COVERAGE_ACTIVE = meteor_parameters.IS_COVERAGE_ACTIVE || process.env['COVERAGE'] === '1'; 11 | export const IS_COVERAGE_VERBOSE = Log.COVERAGE_VERBOSE; 12 | const ENV_NOT_DEFINED = '/SET/ENV/COVERAGE_APP_FOLDER/OR/READ/README/'; 13 | 14 | export const COVERAGE_APP_FOLDER = meteor_parameters.COVERAGE_APP_FOLDER || process.env['COVERAGE_APP_FOLDER'] || ENV_NOT_DEFINED; 15 | 16 | /* istanbul ignore else */ 17 | if (COVERAGE_APP_FOLDER === ENV_NOT_DEFINED) { 18 | Log.info('Error: COVERAGE_APP_FOLDER is undefined and the coverage will fail.'); 19 | } 20 | const NOT_DEFINED = '/COVERAGE/NOT/ACTIVE/'; 21 | let configuration = { 22 | exclude: { 23 | general: [], 24 | server: [], 25 | client: [] 26 | }, 27 | include: [], 28 | output: NOT_DEFINED 29 | }; 30 | /* istanbul ignore else */ 31 | if (IS_COVERAGE_ACTIVE) { 32 | const fs = Npm.require('fs'), 33 | path = Npm.require('path'); 34 | 35 | Log.info('Coverage active'); 36 | let coverageFile = path.join(COVERAGE_APP_FOLDER, '.coverage.json'), 37 | defaultConfig = JSON.parse(Assets.getText('conf/default-coverage.json')); 38 | 39 | try { 40 | fs.accessSync(coverageFile); 41 | Log.info('Reading custom configuration'); 42 | const configurationString = fs.readFileSync(coverageFile); 43 | configuration = JSON.parse(configurationString); 44 | Log.info('[Configuration] ', configuration); 45 | } catch (e) { 46 | if (e instanceof SyntaxError) { 47 | let errMsg = `Error: ${coverageFile} is not a valid JSON`; 48 | console.error(errMsg, e); 49 | Log.error(e.stack); 50 | } 51 | // Set up defaultConfig value if they are not provided in the .coverage.json file 52 | Log.info('Loading default configuration, missing configuration file ', coverageFile); 53 | configuration = defaultConfig; 54 | } 55 | 56 | // Don't force to rewrite all the key of configuration.exclude, 57 | // if they are not defined, the default conf is used. 58 | 59 | /* istanbul ignore else */ 60 | if (configuration.exclude === undefined) { 61 | Log.info('Loading default configuration: exclude.*'); 62 | configuration.exclude = defaultConfig.exclude; 63 | } 64 | 65 | /* istanbul ignore else */ 66 | if (configuration.exclude.general === undefined) { 67 | Log.info('Loading default configuration: exclude.general'); 68 | configuration.exclude.general = defaultConfig.exclude.general; 69 | } 70 | 71 | /* istanbul ignore else */ 72 | if (configuration.exclude.server === undefined) { 73 | Log.info('Loading default configuration: exclude.server'); 74 | configuration.exclude.server = defaultConfig.exclude.server; 75 | } 76 | 77 | /* istanbul ignore else */ 78 | if (configuration.exclude.client === undefined) { 79 | Log.info('Loading default configuration: exclude.client'); 80 | configuration.exclude.client = defaultConfig.exclude.client; 81 | } 82 | 83 | /* istanbul ignore else */ 84 | if (configuration.include === undefined) { 85 | Log.info('Loading default configuration: include'); 86 | configuration.include = defaultConfig.include || []; 87 | } 88 | 89 | /* istanbul ignore else */ 90 | if (configuration.output === undefined) { 91 | Log.info('Loading default configuration: output'); 92 | configuration.output = defaultConfig.output; 93 | } 94 | 95 | /* istanbul ignore else */ 96 | if (configuration.remapFormat === undefined) { 97 | Log.info('Loading default configuration: remapFormat'); 98 | configuration.remapFormat = defaultConfig.remapFormat; 99 | } 100 | } 101 | 102 | export const COVERAGE_EXPORT_FOLDER = configuration.output; 103 | export const exclude = configuration.exclude; 104 | export const include = configuration.include; 105 | export const remapFormat = configuration.remapFormat; 106 | export const reportTypes = { 107 | allowed: ['clover', 'cobertura', 'coverage', 'html', 'json', 'json-summary', 'lcov', 'lcovonly', 'remap', 'teamcity', 'text', 'text-lcov', 'text-summary'], 108 | pending: ['clover', 'cobertura', 'lcov', 'teamcity', 'text', 'text-lcov'] 109 | }; 110 | 111 | Log.info('Coverage configuration:'); 112 | Log.info('- IS_COVERAGE_ACTIVE=', IS_COVERAGE_ACTIVE); 113 | Log.info('- IS_COVERAGE_VERBOSE=', IS_COVERAGE_VERBOSE); 114 | Log.info('- COVERAGE_APP_FOLDER=', COVERAGE_APP_FOLDER); 115 | Log.info('.coverage.json values:'); 116 | Log.info('- exclude=', configuration.exclude); 117 | Log.info('- include=', configuration.include); 118 | Log.info('- remapFormat=', configuration.remapFormat); 119 | Log.info('- COVERAGE_EXPORT_FOLDER=', COVERAGE_EXPORT_FOLDER); 120 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/context/log.js: -------------------------------------------------------------------------------- 1 | const meteor_parameters = { 2 | // /:\ ES 6 3 | // return the value OR UNDEFINED 4 | // THIS IS NOT A BOOLEAN 5 | VERBOSE: Meteor && Meteor.settings && Meteor.settings.coverage && Meteor.settings.coverage.verbose 6 | }; 7 | 8 | export default Log = { 9 | COVERAGE_VERBOSE: meteor_parameters.VERBOSE || process.env['COVERAGE_VERBOSE'] === '1' || false, 10 | error: function() { 11 | console.error(...arguments); 12 | }, 13 | info: function() { 14 | /* istanbul ignore else */ 15 | if (this.COVERAGE_VERBOSE) { 16 | console.log(...arguments); 17 | } 18 | }, 19 | time: function() { 20 | /* istanbul ignore else */ 21 | if (this.COVERAGE_VERBOSE) { 22 | console.log(...arguments); 23 | } 24 | }, 25 | timeEnd: function() { 26 | /* istanbul ignore else */ 27 | if (this.COVERAGE_VERBOSE) { 28 | console.log(...arguments); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/handlers.js: -------------------------------------------------------------------------------- 1 | import Instrumenter from './services/instrumenter'; 2 | import fs from 'fs'; 3 | 4 | instrumentClientJs = function (params, req, res, next) { 5 | var fileurl = req.url.split('?')[0]; 6 | if (Instrumenter.shallInstrumentClientScript(fileurl)) { 7 | var path, 8 | pathLabel; 9 | // Either a package 10 | if (req.url.indexOf('/packages') === 0) { 11 | path = '../web.browser'; 12 | pathLabel = path + fileurl; 13 | } else if (req.url.indexOf('/app') === 0) { 14 | // Or the app/app.js 15 | path = '../web.browser'; 16 | pathLabel = path + fileurl; 17 | } else { 18 | // Or a public file 19 | path = '../web.browser/app'; 20 | pathLabel = path + fileurl; 21 | } 22 | res.setHeader('Content-type', 'application/javascript'); 23 | fs.exists(path + fileurl, function (exists) { 24 | /* istanbul ignore else */ 25 | if (!exists) return next(); 26 | fs.readFile(path + fileurl, 'utf8', function (err, fileContent) { 27 | /* istanbul ignore else */ 28 | if (err) return next(); 29 | Instrumenter.instrumentJs(fileContent, pathLabel, function (err, data) { 30 | /* istanbul ignore else */ 31 | if (err) throw err; 32 | res.end(data); 33 | }); 34 | }); 35 | }); 36 | } else { 37 | next(); 38 | } 39 | }; 40 | 41 | export default Handlers = { 42 | instrumentClientJs 43 | }; 44 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/index.js: -------------------------------------------------------------------------------- 1 | import Conf from './context/conf'; 2 | let library; 3 | 4 | // If the coverage is active, it will import the probe inside this package 5 | // Every script imported using vm.runInThisContext will be hooked by istanbul 6 | // to provide on the fly the instrumented version of each script - in order to generate coverage stats 7 | // You need an external actor like spacejam to run different types of actions automaticaly : 8 | // - merge several types of coverage 9 | // - export reports 10 | if (Conf.IS_COVERAGE_ACTIVE) { 11 | const Lib = require('./main'); 12 | // Provide the real library 13 | library = Lib.default; 14 | } else { 15 | // Mock the library 16 | library = { 17 | Conf, 18 | Router: { 19 | 20 | }, 21 | SourceMap: { 22 | registerSourceMap: function () { 23 | throw 'COVERAGE_NOT_ACTIVE'; 24 | } 25 | }, 26 | CoverageData: { 27 | 28 | }, 29 | Instrumenter: { 30 | hookLoader: function() {} 31 | }, 32 | ReportService: { 33 | 34 | } 35 | }; 36 | } 37 | 38 | export default library; -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/main.js: -------------------------------------------------------------------------------- 1 | import Instrumenter from './services/instrumenter'; 2 | import CoverageData from './services/coverage-data'; 3 | import SourceMap from './services/source-map'; 4 | import Conf from './context/conf'; 5 | import Router from './router'; 6 | import Boot from './boot.js'; 7 | 8 | Boot.startup(); 9 | 10 | export default { 11 | Conf, 12 | Router, 13 | SourceMap, 14 | CoverageData, 15 | Instrumenter 16 | }; 17 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/router.js: -------------------------------------------------------------------------------- 1 | import Handlers from './handlers'; 2 | import Conf from './context/conf'; 3 | import bodyParser from 'body-parser'; 4 | import url from 'url'; 5 | 6 | const handleRequest = (method) => (path, cb) => { 7 | WebApp.rawConnectHandlers.use(path, (req, res, next) => { 8 | if (req.method !== method) { 9 | next(); 10 | return; 11 | } 12 | 13 | const queryString = url.parse(req.url).query || ''; 14 | const queryParams = { query: {} }; 15 | queryString.split('&').forEach((pair) => { 16 | queryParams.query[pair.split('=')[0]] = pair.split('=')[1]; 17 | }); 18 | 19 | Promise.resolve() 20 | .then(() => new Promise(resolve => { 21 | bodyParser.urlencoded({ extended: false })(req, res, resolve); 22 | })) 23 | .then(() => new Promise(resolve => { 24 | bodyParser.json({ limit: '30mb' }).call(null, req, res, resolve); 25 | })) 26 | .then(() => cb(queryParams, req, res, next)) 27 | .catch((e) => { 28 | console.log('Exception undandled:'); 29 | console.log(e.stack); 30 | 31 | next(); 32 | }); 33 | }); 34 | }; 35 | 36 | export default class { 37 | constructor() { 38 | if (Conf.IS_COVERAGE_ACTIVE) { 39 | this.bindRoutes(); 40 | } 41 | } 42 | 43 | bindRoutes() { 44 | // inject istanbul-instruments into js files loaded by the client 45 | handleRequest('GET')('/', Handlers.instrumentClientJs); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/services/core.js: -------------------------------------------------------------------------------- 1 | import Conf from './../context/conf'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | const Coverage = Npm.require('istanbul-lib-coverage'); 5 | 6 | let mergeCoverageWith, importCoverage, getCoverageObject; 7 | 8 | getCoverageObject = function () { 9 | /* istanbul ignore next: default assignment */ 10 | global.__coverage__ = global.__coverage__ || {}; 11 | return global.__coverage__; 12 | }; 13 | 14 | setCoverageObject = function (obj) { 15 | global.__coverage__ = obj; 16 | }; 17 | 18 | mergeCoverageWith = function (obj) { 19 | /* istanbul ignore else */ 20 | if (!obj) { 21 | return; 22 | } 23 | var coverage = getCoverageObject(); 24 | var coverageMap = Coverage.createCoverageMap(coverage); 25 | coverageMap.addFileCoverage(obj); 26 | setCoverageObject(coverageMap.toJSON()); 27 | }; 28 | 29 | 30 | /* istanbul ignore next: default assignment */ 31 | importCoverage = function (res, options = {}) { 32 | Log.info('import coverage'); 33 | /* istanbul ignore next: ternary operator */ 34 | const filename = options.filename ? options.filename : 'report.json'; 35 | const reportPath = path.join(Conf.COVERAGE_APP_FOLDER, Conf.COVERAGE_EXPORT_FOLDER, filename); 36 | fs.exists(reportPath, function (exists) { 37 | /* istanbul ignore else */ 38 | if (!exists) { 39 | res.end(JSON.stringify({ type: 'failed', message: 'report file not found: reportPath=' + reportPath + ' COVERAGE_APP_FOLDER=' + Conf.COVERAGE_APP_FOLDER })); 40 | return; 41 | } 42 | fs.readFile(reportPath, 'utf8', function (err, fileContent) { 43 | /* istanbul ignore else */ 44 | if (err) { 45 | res.end(JSON.stringify({ type: 'failed', message: 'failed to read report file: ' + reportPath })); 46 | return; 47 | } 48 | let coverageObj = JSON.parse(fileContent); 49 | for (let property in coverageObj) { 50 | /* istanbul ignore else */ 51 | if (coverageObj.hasOwnProperty(property)) { 52 | Core.mergeCoverageWith(coverageObj[property]); 53 | } 54 | } 55 | res.end('{"type":"success"}'); 56 | }); 57 | }); 58 | }; 59 | export default Core = { 60 | mergeCoverageWith, 61 | importCoverage, 62 | getCoverageObject 63 | }; 64 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/services/coverage-data.js: -------------------------------------------------------------------------------- 1 | import Conf from './../context/conf'; 2 | import Instrumenter from './instrumenter'; 3 | import SourceMap from './source-map'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | 7 | const Coverage = Npm.require('istanbul-lib-coverage'); 8 | const Report = Npm.require('istanbul-lib-report'); 9 | 10 | export default CoverageData = { 11 | filterCoverageReport: function (report) { 12 | /* istanbul ignore else */ 13 | if (!report.data) { 14 | throw 'Invalid report'; 15 | } 16 | let newData = {}; 17 | for (let property in report.data) { 18 | if (this.isAccepted(property)) { 19 | newData[property] = report.data[property]; 20 | } else { 21 | Log.info('isRefused', property); 22 | } 23 | } 24 | report.data = newData; 25 | return report; 26 | }, 27 | isAccepted: function (filename) { 28 | // Check if the file was also inside a .map 29 | /* istanbul ignore else */ 30 | if (filename.indexOf(Conf.COVERAGE_APP_FOLDER) < 0) { 31 | return false; 32 | } 33 | 34 | let isAServerSideFile = filename.indexOf('client') === -1 && filename.indexOf('web.browser') === -1; 35 | /* istanbul ignore else */ 36 | if (Instrumenter.shouldIgnore(filename, isAServerSideFile)) { 37 | return false; 38 | } 39 | 40 | /* istanbul ignore else */ 41 | if (filename.indexOf('packages/') > 0) { 42 | Log.time('read access ' + filename); 43 | const isExist = fs.existsSync(filename); 44 | Log.timeEnd('read access ' + filename); 45 | /* istanbul ignore else */ 46 | if (isExist) { 47 | // Internal package 48 | return true; 49 | } 50 | } 51 | /* istanbul ignore else */ 52 | if (filename.indexOf('client/') > 0 && filename.indexOf('template.') > 0) { 53 | /* istanbul ignore else */ 54 | if (fs.existsSync(filename)) { 55 | // some file 56 | return true; 57 | } 58 | // this is a html template transformed into js file 59 | return false; 60 | } 61 | /* istanbul ignore else */ 62 | if (filename.indexOf('node_modules') > 0) { 63 | // this is a browser file? 64 | return false; 65 | } 66 | 67 | return true; 68 | }, 69 | getReport: function (coverage) { 70 | let coverageMap = Coverage.createCoverageMap(coverage); 71 | coverageMap = SourceMap.lib.transformCoverage(coverageMap).map; 72 | coverageMap = this.filterCoverageReport(coverageMap); 73 | return coverageMap; 74 | }, 75 | getFileReport: function (coverage, filePath) { 76 | const coverageMap = this.getReport(coverage); 77 | const node = Report.summarizers.flat(coverageMap); 78 | const childs = node.getRoot().getChildren(); 79 | let child; 80 | for (let i = 0; i < childs.length; i++) { 81 | /* istanbul ignore else */ 82 | if (childs[i].getRelativeName() === filePath) { 83 | child = childs[i]; 84 | } 85 | } 86 | return child; 87 | }, 88 | getTreeReport: function (coverage) { 89 | return this.getNodeReport(coverage).getRoot(); 90 | }, 91 | getLcovonlyReport: function (coverage) { 92 | return this.getTreeReport(coverage).getChildren(); 93 | }, 94 | getNodeReport: function (coverage) { 95 | const coverageMap = this.getReport(coverage); 96 | return Report.summarizers.flat(coverageMap); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/services/instrumenter.js: -------------------------------------------------------------------------------- 1 | import {_} from 'meteor/underscore'; 2 | import Log from './../context/log'; 3 | import Conf from './../context/conf'; 4 | import minimatch from 'minimatch'; 5 | const Instrument = Npm.require('istanbul-lib-instrument'), 6 | Hook = Npm.require('istanbul-lib-hook'); 7 | let instrumenter = undefined; 8 | 9 | /** 10 | * hooks `runInThisContext` to add instrumentation to matching files when they are loaded on the server 11 | * @method hookLoader 12 | * @param {Object} opts instrumenter options 13 | */ 14 | hookLoader = function (opts) { 15 | /* istanbul ignore next: default assignment */ 16 | opts = opts || {}; 17 | opts.verbose = true; 18 | opts.coverageVariable = '__coverage__'; // force this always 19 | 20 | /* istanbul ignore else */ 21 | if (instrumenter !== undefined) { 22 | throw 'Instrumenter already defined ! You cannot call this method twice'; 23 | } 24 | instrumenter = Instrument.createInstrumenter(opts); 25 | Hook.hookRunInThisContext( 26 | shallInstrumentServerScript, 27 | function (code, options) { 28 | var filename = typeof options === 'string' ? options : options.filename; 29 | return instrumenter.instrumentSync(code, filename); 30 | }, 31 | { 32 | verbose: opts.verbose 33 | } 34 | ); 35 | }; 36 | 37 | instrumentJs = function (content, path, callback) { 38 | SourceMap.registerSourceMap(path); 39 | return instrumenter.instrument(content, path, callback); 40 | }; 41 | 42 | fileMatch = function (filePath, pattern) { 43 | return minimatch(filePath, pattern, {dot: true}); 44 | }; 45 | shouldIgnore = function (filePath, isAServerSideFile) { 46 | // Force the inclusion of any file using config file 47 | /* istanbul ignore else */ 48 | if (Conf.include) { 49 | /* istanbul ignore else */ 50 | if (Conf.include.some(pattern => Instrumenter.fileMatch(filePath, pattern))) { 51 | Log.info('[Accepted][include]: ', filePath); 52 | return false; 53 | } 54 | } 55 | 56 | /* istanbul ignore else */ 57 | if (Conf.exclude.general) { 58 | /* istanbul ignore else */ 59 | if (Conf.exclude.general.some(pattern => Instrumenter.fileMatch(filePath, pattern))) { 60 | Log.info('[Ignored][exclude.general]: ', filePath); 61 | return true; 62 | } 63 | } 64 | 65 | /* istanbul ignore else */ 66 | if (Conf.exclude.server && isAServerSideFile) { 67 | /* istanbul ignore else */ 68 | if (Conf.exclude.server.some(pattern => Instrumenter.fileMatch(filePath, pattern))) { 69 | Log.info('[Ignored][exclude.server]: ', filePath); 70 | return true; 71 | } 72 | } 73 | 74 | /* istanbul ignore else */ 75 | if (Conf.exclude.client && !isAServerSideFile) { 76 | /* istanbul ignore else */ 77 | if (Conf.exclude.client.some(pattern => Instrumenter.fileMatch(filePath, pattern))) { 78 | Log.info('[Ignored][exclude.client]: ', filePath); 79 | return true; 80 | } 81 | } 82 | 83 | Log.info('[Accepted][*]: ', filePath); 84 | return false; 85 | }; 86 | 87 | shallInstrumentClientScript = function (fileurl) { 88 | /* istanbul ignore else */ 89 | if (fileurl.indexOf('.js') > -1) { 90 | /* istanbul ignore else */ 91 | if (!Instrumenter.shouldIgnore(fileurl, false)) { 92 | Log.info('[ClientSide][Public] file instrumented: ' + fileurl); 93 | return true; 94 | } 95 | Log.info('[ClientSide][Public] file ignored: ' + fileurl); 96 | return false; 97 | } 98 | return false; 99 | }; 100 | 101 | 102 | /** 103 | * 104 | * a match function with signature `fn(file)` that returns true if `file` needs to be instrumented 105 | * if the result is true, it also reads the corresponding source map 106 | * @returns {Function} 107 | */ 108 | shallInstrumentServerScript = function (file) { 109 | var root = __meteor_bootstrap__.serverDir; 110 | /* istanbul ignore else */ 111 | if (file.indexOf(root) !== 0) { 112 | Log.info('[ServerSide][OutsideApp] file ignored: ' + file); 113 | return false; 114 | } 115 | file = file.substring(root.length); 116 | /* istanbul ignore else */ 117 | if (file.indexOf('node_modules') >= 0) { 118 | Log.info('[ServerSide][node_modules] file ignored: ' + file); 119 | return false; 120 | } 121 | if (file.indexOf('packages') === 1) { 122 | /* istanbul ignore else */ 123 | if (!Instrumenter.shouldIgnore(file, true)) { 124 | SourceMap.registerSourceMap(root + file); 125 | return true; 126 | } 127 | } else { 128 | /* istanbul ignore else */ 129 | if (!Instrumenter.shouldIgnore(root + file, true)) { 130 | SourceMap.registerSourceMap(root + file); 131 | return true; 132 | } 133 | } 134 | 135 | return false; 136 | }; 137 | 138 | export default Instrumenter = { 139 | hookLoader, 140 | instrumentJs, 141 | shouldIgnore, 142 | fileMatch, 143 | shallInstrumentClientScript, 144 | shallInstrumentServerScript 145 | }; 146 | -------------------------------------------------------------------------------- /meteor-legacy-coverage/server/services/source-map.js: -------------------------------------------------------------------------------- 1 | import Log from './../context/log'; 2 | import Conf from './../context/conf'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | const homedir = Npm.require('homedir'); 7 | const libSourceMaps = Npm.require('istanbul-lib-source-maps'); 8 | 9 | const sourceMap = libSourceMaps.createSourceMapStore({verbose: Conf.IS_COVERAGE_ACTIVE}); 10 | const meteorDir = Conf.COVERAGE_APP_FOLDER; 11 | const splitToken = String.fromCharCode(56507) + 'app/'; // caution! magic character inside the SourceMap source(s) path 12 | 13 | const abspath = { 14 | local: path.join(__meteor_bootstrap__.serverDir, '..', '..', '..'), // could use process.env.METEOR_SHELL_DIR too 15 | currentBuild: path.join(__meteor_bootstrap__.serverDir, '..'), 16 | serverSide: __meteor_bootstrap__.serverDir, 17 | clientSide: path.join(__meteor_bootstrap__.serverDir, '..', 'web.browser'), 18 | // Meteor packages folder can be overriden with the env var PACKAGE_DIRS, otherwise '$HOME/.meteor/packages'. 19 | // Read https://guide.meteor.com/writing-atmosphere-packages.html#overriding-atmosphere-packages 20 | packages: process.env.PACKAGE_DIRS || path.join(homedir(), '.meteor', 'packages') 21 | }; 22 | const rgx = { 23 | meteorCompiledTemplate: /\/template\.[^\.\/]+\.js$/, 24 | meteorPackageMergedFile: /^\/packages\/(local-test_)?(?:([^\/_]+)_)?([^\/_]+).js$/, 25 | meteorPackagePathTokens: /^(?:packages\/|node_modules\/meteor\/)(?:local-test[_:])?(([^_:\/]+[_:])?([^_:\/]+))\/(.*node_modules\/)?(.*)$/, 26 | meteorPUT: /^local-test:((?:[^_:\/]+:)?[^_:\/]+)$/, 27 | packageJson: /^(?:\.\.\/npm\/node_modules\/(.*)|\.\.\/\.\.\/(?:(?!node_modules).)*(.*)|.*node_modules\/(.*))$/ 28 | }; 29 | 30 | isAccessible = function(path, mode = fs.R_OK, supressErrors = false) { 31 | try { 32 | fs.accessSync(path, mode); 33 | return true; 34 | } catch (e) { 35 | /* istanbul ignore else */ 36 | if (!supressErrors) { 37 | Log.error('Cannot access', path); 38 | } 39 | return false; 40 | } 41 | }; 42 | 43 | parseJSON = function(filePath, supressAccessErrors = false) { 44 | /* istanbul ignore else */ 45 | if (isAccessible(filePath, fs.R_OK, supressAccessErrors)) { 46 | try { 47 | return JSON.parse(fs.readFileSync(filePath, 'utf8')); 48 | } catch(e) { 49 | /* istanbul ignore next: Meteor should have saved an invalid JSON, quite improbable */ 50 | Log.error('Invalid JSON:', filePath, e); 51 | } 52 | } 53 | }; 54 | 55 | initialSetup = function () { 56 | // Get the resolved, compiled and used packages and their versions 57 | let resolverResultPath = path.join(abspath.local, 'resolver-result-cache.json'); 58 | let resolverResult = parseJSON(resolverResultPath); 59 | /* istanbul ignore next: ternary operator */ 60 | this.resolved = resolverResult ? resolverResult.lastOutput.answer : null; 61 | 62 | /* istanbul ignore else */ 63 | if (Meteor.isPackageTest) { 64 | /* istanbul ignore else */ 65 | if (this.resolved) { 66 | // Find the package(s) under test (PUT) 67 | for (let pkg in this.resolved) { 68 | /* istanbul ignore else */ 69 | if (this.resolved.hasOwnProperty(pkg)) { 70 | let match = rgx.meteorPUT.exec(pkg); 71 | match && (this.PUT[match[1]] = true); 72 | } 73 | } 74 | const PUTs = Object.keys(this.PUT); 75 | /* istanbul ignore else */ 76 | if (PUTs.length) { 77 | Log.info(`Packages under test (${PUTs.length}):`, PUTs.join(', ')); 78 | } else { 79 | Log.error('No packages under test in test-packages mode'); 80 | } 81 | } 82 | 83 | // Check if testing from inside (pkg/) or outside (app/). We test all the merged files of meteor package(s) tests(s) 84 | // assuming that `meteor test-packages` was exec from the package folder which the merged test file belongs to: 85 | // - If none can be accessed, then command is exec from app-dir 86 | // - If one can be accessed, then command is exec from pkg-dir of that package 87 | // The possibilities when testing packages are: 88 | // 1. `app/packages/pkg$ meteor test-packages ...opts` inside (COVERAGE_APP_FOLDER points to app/packages/pkg/), test N packages 89 | // 2. `app/packages/pkg$ meteor test-packages ./ ...opts` inside (COVERAGE_APP_FOLDER points to app/packages/pkg/), test 1 package 90 | // 3. `app$ meteor test-packages ...opts` outside (COVERAGE_APP_FOLDER points to app/), test N packages 91 | // 4. `app$ meteor test-packages author:pkg...opts` outside (COVERAGE_APP_FOLDER points to app/), test 1 package 92 | // 5. `app$ meteor test-packages packages/pkg...opts` outside (COVERAGE_APP_FOLDER points to app/), test 1 package 93 | // NOTE: `...opts` represents the remaining command options (`--driver-package`, etc.) 94 | const sidePaths = { 95 | load: abspath.serverSide, 96 | manifest: abspath.clientSide 97 | }; 98 | for (let key in sidePaths) { 99 | /* istanbul ignore else */ 100 | if (sidePaths.hasOwnProperty(key)) { 101 | const programPath = path.join(sidePaths[key], 'program.json'); 102 | const program = parseJSON(programPath); 103 | /* istanbul ignore next: file automatically created by Meteor, so really rare to enter here */ 104 | if (!program) continue; 105 | 106 | for (let file of program[key]) { 107 | let isTestFile, matchAuthor, matchName, match = rgx.meteorPackageMergedFile.exec(`/${file.path}`); 108 | // If it's a meteor package test(s) merged file and the package has tests (the merged file is created whether 109 | // the package has tests file(s) declared in `package.js` inside `Package.onTest()` or not). The way to know 110 | // whether the package has tests or not is looking at file.sourceMap: if it's empty, it has no tests. 111 | /* istanbul ignore else */ 112 | if (match && match[1] && file.sourceMap) { 113 | [, isTestFile, matchAuthor, matchName] = match; 114 | const sourceMapPath = path.join(sidePaths[key], file.sourceMap); 115 | const sourceMap = parseJSON(sourceMapPath); 116 | /* istanbul ignore else */ 117 | if (!sourceMap) continue; // jump to the next file if SourceMap non-accessible or invalid 118 | 119 | // A compiled test file (local-test_...) has only the declared test 120 | // files inside `package.js` as its sources, so check the first one 121 | this.testingFromPackageDir = `${matchAuthor}:${matchName}`; 122 | let filepathToCheck = fixSourcePath.call(this, sourceMap.sources[0], null, meteorDir); 123 | if (!isAccessible(filepathToCheck, fs.R_OK, true)) { 124 | this.testingFromPackageDir = false; 125 | } else { 126 | break; 127 | } 128 | } 129 | } 130 | } 131 | /* istanbul ignore else */ 132 | if (this.testingFromPackageDir) break; 133 | } 134 | } 135 | }; 136 | 137 | // Alter inside the source map the path of each sources 138 | alterSourceMapPaths = function (map, isClientSide) { 139 | // Absolute path to sources of a Meteor package. PUTs are treated differently than normal because they 140 | // might not exist at abspath.packages, so PUT packages always are resolved depending on 141 | // COVERAGE_APP_FOLDER and whether `meteor test-packages` was executed from inside/outside the package 142 | // folder. Sources base of any Meteor packages not under test is always resolved to abspath.packages 143 | let sourcesBase, isTestFile, matchAuthor, matchName; 144 | /* istanbul ignore else */ 145 | if (rgx.meteorPackageMergedFile.test(map.file)) { 146 | [, isTestFile, matchAuthor, matchName] = rgx.meteorPackageMergedFile.exec(map.file); 147 | /* istanbul ignore next: ternary operator */ 148 | const packageID = matchAuthor ? `${matchAuthor}:${matchName}` : matchName; 149 | if (Meteor.isPackageTest && (!!isTestFile || this.PUT[packageID])) { 150 | // If exec `meteor test-packages` from `meteor-app-dir/packages/pkg-dir/` then Meteor performs tests on ALL 151 | // packages at `meteor-app-dir/packages`, just like exec `meteor test-packages` from `meteor-app-dir/`, but 152 | // this affects `sourcesBase` for PUTs, because PUTs outside COVERAGE_APP_FOLDER must change their sourcesBase 153 | if (this.testingFromPackageDir) { 154 | sourcesBase = this.testingFromPackageDir === packageID ? meteorDir : path.join(meteorDir, '..', matchName); 155 | } else { 156 | sourcesBase = path.join(meteorDir, 'packages', matchName); 157 | } 158 | } else { 159 | /* istanbul ignore else */ 160 | if (this.resolved[packageID]) { 161 | /* istanbul ignore next: ternary operator */ 162 | const packageFolder = matchAuthor ? `${matchAuthor}_${matchName}` : matchName; 163 | sourcesBase = path.join(abspath.packages, packageFolder, this.resolved[packageID], 'web.browser'); 164 | } 165 | } 166 | } 167 | 168 | // Get `node_modules` base path for this map.file 169 | let nodeModulesBase, program = parseJSON(path.join(abspath.serverSide, 'program.json')); 170 | /* istanbul ignore else */ 171 | if (!isClientSide && program) { 172 | // Find the item matching map.file path 173 | const mergedPath = map.file.substr(1); 174 | for (let file of program.load) { 175 | /* istanbul ignore else */ 176 | if (file.path === mergedPath) { 177 | /* istanbul ignore else */ 178 | if (file.node_modules) { 179 | try { 180 | // For the current package it appears it can be an object, with two slightly different paths. 181 | if (typeof file.node_modules === 'object') { 182 | for (var nModulePath in file.node_modules) { 183 | // there is several files, just need to find the local{bool} having true 184 | if (file.node_modules[nModulePath].local) { 185 | nodeModulesBase = path.join(abspath.serverSide, nModulePath); 186 | break; // cut the loop 187 | } 188 | } 189 | } else { 190 | nodeModulesBase = path.join(abspath.serverSide, file.node_modules); 191 | } 192 | nodeModulesBase = fs.realpathSync(nodeModulesBase); // usually a symlink 193 | } catch (e) { 194 | if (e.code === 'ENOENT') { 195 | Log.info('File not found!', nodeModulesBase); 196 | } else { 197 | throw e; 198 | } 199 | } 200 | } 201 | break; 202 | } 203 | } 204 | } 205 | /* istanbul ignore else */ 206 | if (!nodeModulesBase && sourcesBase) { 207 | // Try locating node_modules inside sourcesBase sibling `npm` 208 | let sourcesSiblingFolder = path.join(sourcesBase, '..', 'npm', 'node_modules'); 209 | /* istanbul ignore else */ 210 | if (isAccessible(sourcesSiblingFolder, fs.R_OK, true)) { 211 | nodeModulesBase = sourcesSiblingFolder; 212 | } 213 | } 214 | 215 | // Fix sources paths, but be aware that, although you might be tempted to remove items 216 | // from map.{sources|contentSources} (like non-instrumentable files: *.css, *.json,...), 217 | // you must NOT do it, because their indexes are still being used by the mappings and 218 | // you'll get a sound `Error('No element indexed by {index}')`. 219 | for (let i = 0; i < map.sources.length; i++) { 220 | // Meteor templates are not saved into files, but included in sourcesContent 221 | /* istanbul ignore else */ 222 | if (rgx.meteorCompiledTemplate.test(map.sources[i])) { 223 | Log.info('Skipping Meteor template:', map.sources[i]); 224 | continue; 225 | } 226 | 227 | let fixed = fixSourcePath.call(this, map.sources[i], nodeModulesBase, sourcesBase); 228 | 229 | if (map.sources[i] === fixed) { 230 | Log.error('Source could not be altered:', map.sources[i]); 231 | } else if (isAccessible(fixed)) { 232 | map.sources[i] = fixed; 233 | } else { 234 | Log.error('Altered source could not be accessed:', map.sources[i]); 235 | } 236 | } 237 | return map; 238 | }; 239 | 240 | // Fixes path of a source (file) in the SourceMap of a concatenated Meteor package test file 241 | fixSourcePath = function(source, nodeModulesBase, sourcesBase) { 242 | let match, paths = source.split(splitToken).slice(1); 243 | 244 | // Skip sources with unknown syntax 245 | /* istanbul ignore else */ 246 | if (!paths.length) { 247 | Log.error('Source with unknown format:', source); 248 | return source; 249 | } 250 | 251 | // The source is the package.json of a NPM dependency. Catches all next patterns: 252 | // 1. meteor://💻app/.npm/package/node_modules/minimatch/package.json 253 | // [1 may be @ nodeModulesBase (when non-PUT) or sourcesBase (when PUT)] 254 | // 2. meteor://💻app/../npm/node_modules/meteor-babel-helpers/package.json (package NPM dep) 255 | // 3. meteor://💻app/../../app-dir/node_modules/meteor-node-stubs/node_modules/string_decoder/package.json (app NPM dep) 256 | // 4. meteor://💻app/node_modules/http-errors/node_modules/inherits/package.json 257 | // 5. meteor://💻app/node_modules/content-type/package.json 258 | /* istanbul ignore else */ 259 | if (paths[0].endsWith('/package.json')) { 260 | match = rgx.packageJson.exec(paths[0]); 261 | /* istanbul ignore else */ 262 | if (match) { 263 | /* istanbul ignore else */ 264 | if (match[2]) { // covers 3 (app NPM dep package.json) 265 | return path.join(meteorDir, match[2]); 266 | } 267 | /* istanbul ignore else */ 268 | if (match[3] && nodeModulesBase) { 269 | // covers 1 (when non-PUT), 4 and 5 (meteor pkg NPM dep package.json) 270 | return path.join(nodeModulesBase, match[3]); 271 | } 272 | return path.join(sourcesBase, paths[0]); // covers 1 (when PUT) and 2 273 | } 274 | } 275 | 276 | // The source is a Meteor package file (NPM dep or own file). Catches all next patterns: 277 | // 6. meteor://💻app/packages/lmieulet:meteor-coverage/server/index.js 278 | // 7. meteor://💻app/packages/local-test:lmieulet:meteor-coverage/server/tests.js 279 | // 8. meteor://💻app/node_modules/meteor/lmieulet:meteor-coverage/node_modules/minimatch/minimatch.js 280 | // 9. meteor://💻app/node_modules/meteor/local-test:cgalvarez:my-package/tests/client/mocks.js 281 | // 10. meteor://💻app/node_modules/meteor/local-test:cgalvarez:my-package/node_modules/chai-as-promised/lib/chai-as-promised.js 282 | // 11. meteor://💻app/node_modules/meteor/local-test:kadira:flow-router/node_modules/page/node_modules/path-to-regexp/node_modules/isarray/index.js 283 | let matchPackageID, matchAuthor, matchName, matchNpmDepPath, matchPath; 284 | match = rgx.meteorPackagePathTokens.exec(paths[0]); 285 | /* istanbul ignore else */ 286 | if (match) { 287 | [, matchPackageID, matchAuthor, matchName, matchNpmDepPath, matchPath] = match; 288 | /* istanbul ignore else */ 289 | if (this.PUT[matchPackageID]) { // PUT 290 | /* istanbul ignore else */ 291 | if (matchNpmDepPath) { 292 | // There is no way to know a priori if it's a recursive dep or not 293 | let recNpmDep = path.join(sourcesBase, '.npm', 'package', matchNpmDepPath, matchPath); 294 | /* istanbul ignore else */ 295 | if (isAccessible(recNpmDep, fs.R_OK, true)) { 296 | return recNpmDep; // check if recursive dep (11) of PUT 297 | } 298 | return path.join(sourcesBase, '.npm', 'package', 'node_modules', matchPath); // first level dep (10) of PUT 299 | } 300 | return path.join(sourcesBase, matchPath); // covers 6,7,8,9 when PUT 301 | } 302 | 303 | /* istanbul ignore else */ 304 | if (Meteor.isPackageTest) { 305 | return path.join(matchNpmDepPath ? nodeModulesBase : sourcesBase, matchPath); // non PUT 306 | } 307 | 308 | // Package inside app-dir/packages on `meteor test ...` 309 | return path.join(meteorDir, 'packages', matchName, matchPath); 310 | } 311 | 312 | // Meteor app file 313 | return path.join(meteorDir, paths[0]); 314 | }; 315 | 316 | // Processes the source map (when exists) of an instrumented file to fix broken sources paths 317 | registerSourceMap = function(filepath) { 318 | const sourceMapPath = filepath + '.map'; 319 | let fileContent = parseJSON(sourceMapPath, true); 320 | if (fileContent) { 321 | Log.time('registerSourceMap', filepath); 322 | fileContent = alterSourceMapPaths.call(this, fileContent, 323 | filepath.startsWith('../web.browser/') || filepath.startsWith(abspath.clientSide)); 324 | Log.info('Add source map for file', sourceMapPath); 325 | sourceMap.registerMap(filepath, fileContent); 326 | Log.timeEnd('registerSourceMap', filepath); 327 | } else { 328 | Log.info('Source map not found', sourceMapPath); 329 | } 330 | }; 331 | 332 | export default SourceMap = { 333 | initialSetup, 334 | lib: sourceMap, 335 | PUT: {}, // Meteor package(s) under test 336 | registerSourceMap, 337 | resolved: undefined, // Meteor packages in use and their version 338 | testingFromPackageDir: undefined // Whether `meteor test-packages` is run from inside/outside the package dir 339 | }; 340 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'lmieulet:meteor-coverage', 3 | version: '4.1.0', 4 | summary: 'Server and client coverage for Meteor', 5 | git: 'https://github.com/serut/meteor-coverage', 6 | documentation: 'README.md', 7 | debugOnly: true // this package is not included on prod 8 | }); 9 | 10 | const dependencies = { 11 | 'istanbul-lib-coverage': '2.0.1', 12 | 'istanbul-lib-report': '2.0.2', 13 | 'istanbul-reports': '2.0.1', 14 | 'body-parser': '1.18.3', 15 | 'mkdirp': '0.5.1', 16 | 'remap-istanbul': '0.6.4' 17 | }; 18 | 19 | Package.onUse(function (api) { 20 | api.versionsFrom(['2.3']); 21 | 22 | api.use(['ecmascript']); 23 | api.use('webapp', 'server'); 24 | api.use('http', 'client'); 25 | // Add datasets 26 | api.addAssets('conf/default-coverage.json', 'server'); 27 | 28 | // Istanbul assets files - because we do not have the link to these files anymore in the istanbul v1.0 29 | api.addAssets([ 30 | 'assets/vendor/prettify.css', 31 | 'assets/vendor/prettify.js', 32 | 'assets/base.css', 33 | 'assets/sort-arrow-sprite.png', 34 | 'assets/sorter.js', 35 | 'assets/block-navigation.js' 36 | ], 'server'); 37 | 38 | api.mainModule('server/index.js', 'server'); 39 | api.mainModule('client/methods.js', 'client'); 40 | Npm.depends(dependencies); 41 | }); 42 | 43 | 44 | Package.onTest(function (api) { 45 | api.use('ecmascript'); 46 | api.use('lmieulet:meteor-legacy-coverage@0.2.0', 'server'); 47 | api.use('http', 'client'); 48 | api.use('webapp', 'server'); 49 | api.use(['lmieulet:meteor-coverage']); 50 | api.use(['meteortesting:mocha']); 51 | // New meteor 12/2018 unknown issue 52 | api.addFiles(['client/methods.e2e.tests.js', 'client/methods.unit.tests.js', 'client/client.instrumentation.tests.js'], 'client'); 53 | api.mainModule('server/tests.js', 'server'); 54 | api.mainModule('client/main.tests.js', 'client'); 55 | 56 | Npm.depends({ 57 | ...dependencies, 58 | 'chai': '4.2.0', 59 | 'sinon': '7.1.1', 60 | 'sinon-chai': '3.2.0' 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-coverage", 3 | "version": "4.1.0", 4 | "description": "A meteor package that allows you to get the statement, line, function and branch coverage of Meteor project. This package uses the [istanbuljs](https://github.com/istanbuljs/istanbuljs) packages for coverage report. It's a debug only package, so it does not affect your production build.", 5 | "main": "server/index.js", 6 | "dependencies": { 7 | "body-parser": "1.18.3", 8 | "istanbul-lib-coverage": "^2.0.1", 9 | "istanbul-lib-report": "^2.0.2", 10 | "istanbul-reports": "^2.0.1", 11 | "mkdirp": "0.5.1", 12 | "remap-istanbul": "0.12.0" 13 | }, 14 | "devDependencies": { 15 | "babel-eslint": "^8.2.6", 16 | "chai": "^4.2.0", 17 | "eslint": "^4.19.1", 18 | "eslint-plugin-babel": "^5.2.1", 19 | "eslint-plugin-import": "^2.14.0", 20 | "eslint-plugin-mocha": "^5.2.0", 21 | "eslint-plugin-promise": "^4.0.1" 22 | }, 23 | "scripts": { 24 | "setup-test": "rm -rf ./someapp && meteor create --bare someapp && cd someapp && cp ../.coverage.json . && meteor npm i --save puppeteer && mkdir packages && ln -s ../../ ./packages/meteor-coverage", 25 | "test": "meteor npm run setup-test && cd someapp && TEST_BROWSER_DRIVER=puppeteer COVERAGE_VERBOSE=1 COVERAGE=1 COVERAGE_OUT_LCOVONLY=1 COVERAGE_APP_FOLDER=$(pwd)/ meteor test-packages --once --driver-package meteortesting:mocha ./packages/meteor-coverage", 26 | "test:watch": "cd someapp && TEST_WATCH=1 COVERAGE_VERBOSE=1 COVERAGE=1 COVERAGE_APP_FOLDER=$(pwd)/ meteor test-packages --driver-package meteortesting:mocha ./packages/meteor-coverage", 27 | "start": "meteor npm run lint:fix & meteor npm run test:watch", 28 | "lint": "eslint .", 29 | "lint:fix": "eslint --fix .", 30 | "publish": "METEOR_FORCE_PORTABLE=1 meteor publish", 31 | "todobeforepublish": "rm -rf .npm/ node_modules/ meteor-legacy-coverage/ .npm/ .vscode/ .circleci/ .github/ someapp/ .travis.yml .versions .eslint*" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/serut/meteor-coverage.git" 36 | }, 37 | "author": "Léo Mieulet", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/serut/meteor-coverage/issues" 41 | }, 42 | "homepage": "https://github.com/serut/meteor-coverage#readme" 43 | } 44 | -------------------------------------------------------------------------------- /server/boot.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import mkdirp from 'mkdirp'; 4 | import Conf from './context/conf'; 5 | import Router from './router'; 6 | 7 | export default Boot = { 8 | startup() { 9 | // Create reports output folder if not exists 10 | let outputFolder = path.join(Conf.COVERAGE_APP_FOLDER, Conf.COVERAGE_EXPORT_FOLDER); 11 | fs.access(outputFolder, fs.F_OK | fs.R_OK | fs.W_OK, (err) => { 12 | /* istanbul ignore else */ 13 | if (err) { 14 | try { 15 | mkdirp(outputFolder); 16 | } catch (e) { 17 | console.error (`meteor-coverage failed to create the folder ${outputFolder} while booting:`, e); 18 | Log.error(e.stack); 19 | } 20 | } 21 | }); 22 | // Connect the router to this app 23 | new Router(); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /server/context/conf.js: -------------------------------------------------------------------------------- 1 | import Log from './log'; 2 | const meteor_parameters = { 3 | // /:\ ES 6 4 | // return the value OR UNDEFINED 5 | // THIS IS NOT A BOOLEAN 6 | IS_COVERAGE_ACTIVE: Meteor && Meteor.settings && Meteor.settings.coverage && Meteor.settings.coverage.is_coverage_active, 7 | COVERAGE_APP_FOLDER: Meteor && Meteor.settings && Meteor.settings.coverage && Meteor.settings.coverage.coverage_app_folder 8 | }; 9 | 10 | export const IS_COVERAGE_ACTIVE = meteor_parameters.IS_COVERAGE_ACTIVE || process.env['COVERAGE'] === '1'; 11 | export const IS_COVERAGE_VERBOSE = Log.COVERAGE_VERBOSE; 12 | const ENV_NOT_DEFINED = '/SET/ENV/COVERAGE_APP_FOLDER/OR/READ/README/'; 13 | 14 | export const COVERAGE_APP_FOLDER = meteor_parameters.COVERAGE_APP_FOLDER || process.env['COVERAGE_APP_FOLDER'] || ENV_NOT_DEFINED; 15 | 16 | /* istanbul ignore else */ 17 | if (COVERAGE_APP_FOLDER === ENV_NOT_DEFINED) { 18 | Log.info('Error: COVERAGE_APP_FOLDER is undefined and the coverage will fail.'); 19 | } 20 | const NOT_DEFINED = '/COVERAGE/NOT/ACTIVE/'; 21 | let configuration = { 22 | output: NOT_DEFINED 23 | }; 24 | /* istanbul ignore else */ 25 | if (IS_COVERAGE_ACTIVE) { 26 | const fs = Npm.require('fs'), 27 | path = Npm.require('path'); 28 | 29 | Log.info('Coverage active'); 30 | let coverageFile = path.join(COVERAGE_APP_FOLDER, '.coverage.json'), 31 | defaultConfig = JSON.parse(Assets.getText('conf/default-coverage.json')); 32 | 33 | try { 34 | fs.accessSync(coverageFile); 35 | Log.info('Reading custom configuration'); 36 | const configurationString = fs.readFileSync(coverageFile); 37 | configuration = JSON.parse(configurationString); 38 | Log.info('[Configuration] ', configuration); 39 | } catch (e) { 40 | if (e instanceof SyntaxError) { 41 | let errMsg = `Error: ${coverageFile} is not a valid JSON`; 42 | console.error(errMsg, e); 43 | Log.error(e.stack); 44 | } 45 | // Set up defaultConfig value if they are not provided in the .coverage.json file 46 | Log.info('Loading default configuration, missing configuration file ', coverageFile); 47 | configuration = defaultConfig; 48 | } 49 | 50 | // Don't force to rewrite all the key of configuration. 51 | 52 | /* istanbul ignore else */ 53 | if (configuration.output === undefined) { 54 | Log.info('Loading default configuration: output'); 55 | configuration.output = defaultConfig.output; 56 | } 57 | 58 | /* istanbul ignore else */ 59 | if (configuration.remapFormat === undefined) { 60 | Log.info('Loading default configuration: remapFormat'); 61 | configuration.remapFormat = defaultConfig.remapFormat; 62 | } 63 | } 64 | 65 | export const COVERAGE_EXPORT_FOLDER = configuration.output; 66 | export const remapFormat = configuration.remapFormat; 67 | export const reportTypes = { 68 | allowed: ['clover', 'cobertura', 'coverage', 'html', 'json', 'json-summary', 'lcov', 'lcovonly', 'remap', 'teamcity', 'text', 'text-lcov', 'text-summary'], 69 | pending: ['clover', 'cobertura', 'lcov', 'teamcity', 'text', 'text-lcov'] 70 | }; 71 | 72 | Log.info('Coverage configuration:'); 73 | Log.info('- IS_COVERAGE_ACTIVE=', IS_COVERAGE_ACTIVE); 74 | Log.info('- IS_COVERAGE_VERBOSE=', IS_COVERAGE_VERBOSE); 75 | Log.info('- COVERAGE_APP_FOLDER=', COVERAGE_APP_FOLDER); 76 | Log.info('.coverage.json values:'); 77 | Log.info('- remapFormat=', configuration.remapFormat); 78 | Log.info('- COVERAGE_EXPORT_FOLDER=', COVERAGE_EXPORT_FOLDER); 79 | -------------------------------------------------------------------------------- /server/context/log.js: -------------------------------------------------------------------------------- 1 | const meteor_parameters = { 2 | // /:\ ES 6 3 | // return the value OR UNDEFINED 4 | // THIS IS NOT A BOOLEAN 5 | VERBOSE: Meteor && Meteor.settings && Meteor.settings.coverage && Meteor.settings.coverage.verbose 6 | }; 7 | 8 | export default Log = { 9 | COVERAGE_VERBOSE: meteor_parameters.VERBOSE || process.env['COVERAGE_VERBOSE'] === '1' || false, 10 | error: function() { 11 | console.error(...arguments); 12 | }, 13 | info: function() { 14 | /* istanbul ignore else */ 15 | if (this.COVERAGE_VERBOSE) { 16 | console.log(...arguments); 17 | } 18 | }, 19 | time: function() { 20 | /* istanbul ignore else */ 21 | if (this.COVERAGE_VERBOSE) { 22 | console.log(...arguments); 23 | } 24 | }, 25 | timeEnd: function() { 26 | /* istanbul ignore else */ 27 | if (this.COVERAGE_VERBOSE) { 28 | console.log(...arguments); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /server/handlers.js: -------------------------------------------------------------------------------- 1 | import Conf from './context/conf'; 2 | import Core from './services/core'; 3 | import ReportService from './report/report-service'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | 7 | showCoverage = function (params, req, res, next) { 8 | let options = { 9 | 'filepath': params.query.p 10 | }; 11 | let reportService = new ReportService(); 12 | reportService.generateReport(res, 'http', options); 13 | }; 14 | 15 | getAsset = function (params, req, res, next) { 16 | var assetsDir = path.join(path.resolve('.'), 'assets/packages/lmieulet_meteor-coverage/assets/'), 17 | filename = params.filename; 18 | fs.exists(path.join(assetsDir, filename), function (exists) { 19 | if (!exists) { 20 | fs.exists(path.join(assetsDir, '/vendor/', filename), function (exists) { 21 | /* istanbul ignore else */ 22 | if (!exists) return next(); 23 | fs.readFile(assetsDir + '/vendor/' + filename, function (err, fileContent) { 24 | /* istanbul ignore else */ 25 | if (err) { 26 | console.error(err); 27 | return next(); 28 | } 29 | res.end(fileContent); 30 | }); 31 | }); 32 | } else { 33 | fs.readFile(assetsDir + '/' + filename, function (err, fileContent) { 34 | /* istanbul ignore else */ 35 | if (err) { 36 | console.error(err); 37 | return next(); 38 | } 39 | res.end(fileContent); 40 | }); 41 | } 42 | }); 43 | }; 44 | 45 | addClientCoverage = function (params, req, res, next) { 46 | var body = req.body; 47 | /* istanbul ignore else */ 48 | if (!body) { 49 | res.writeHead(400); 50 | res.end(); 51 | } 52 | 53 | var clientCoverage; 54 | for (var property in body) { 55 | /* istanbul ignore else */ 56 | if (body.hasOwnProperty(property)) { 57 | clientCoverage = body[property]; 58 | } 59 | } 60 | if (clientCoverage) { 61 | Core.mergeCoverageWith(clientCoverage); 62 | res.end('{"type":"success"}'); 63 | } else { 64 | res.writeHead(400); 65 | res.end('Nothing has been imported'); 66 | } 67 | }; 68 | 69 | exportFile = function (params, req, res, next) { 70 | var _type = params.type; 71 | /* istanbul ignore next: ternary operator */ 72 | type = Conf.reportTypes.allowed.indexOf(_type) > -1 ? _type : 'coverage'; 73 | try { 74 | let reportService = new ReportService(); 75 | reportService.generateReport(res, type, {}); 76 | } catch (e) { 77 | Log.error('Failed to export', e, e.stack); 78 | res.writeHead(400); 79 | res.end('Nothing has been export'); 80 | } 81 | }; 82 | importCoverage = function (params, req, res, next) { 83 | try { 84 | Core.importCoverage(res); 85 | } catch (e) { 86 | Log.error('Failed to import', e, e.stack); 87 | res.writeHead(400); 88 | res.end('No file has been import'); 89 | } 90 | }; 91 | 92 | export default Handlers = { 93 | showCoverage, 94 | getAsset, 95 | addClientCoverage, 96 | exportFile, 97 | importCoverage 98 | }; 99 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import Conf from './context/conf'; 2 | let library; 3 | 4 | // If the coverage is active, it will import the probe inside this package 5 | // Every script imported using vm.runInThisContext will be hooked by istanbul 6 | // to provide on the fly the instrumented version of each script - in order to generate coverage stats 7 | // You need an external actor like spacejam to run different types of actions automaticaly : 8 | // - merge several types of coverage 9 | // - export reports 10 | if (Conf.IS_COVERAGE_ACTIVE) { 11 | const Lib = require('./main'); 12 | // Provide the real library 13 | library = Lib.default; 14 | } else { 15 | // Mock the library 16 | library = { 17 | Conf, 18 | Router: { 19 | 20 | }, 21 | CoverageData: { 22 | 23 | }, 24 | ReportService: { 25 | 26 | } 27 | }; 28 | } 29 | 30 | export default library; 31 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import CoverageData from './services/coverage-data'; 2 | import Conf from './context/conf'; 3 | import Router from './router'; 4 | import ReportService from './report/report-service'; 5 | import Boot from './boot.js'; 6 | 7 | Boot.startup(); 8 | 9 | export default { 10 | Conf, 11 | Router, 12 | CoverageData, 13 | ReportService 14 | }; 15 | -------------------------------------------------------------------------------- /server/report/report-common.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import mkdirp from 'mkdirp'; 3 | import path from 'path'; 4 | import Log from './../context/log'; 5 | const Report = Npm.require('istanbul-lib-report'); 6 | 7 | 8 | export default ReportCommon = { 9 | /** 10 | * Alter fs to add a new method writer 11 | * Used by the istanbul-reports library 12 | * @param filepath 13 | * @returns {*|Context} 14 | */ 15 | 16 | getContext (filepath) { 17 | let context = Report.createContext(); 18 | 19 | const dirpath = path.dirname(filepath); 20 | this.checkDirectory(dirpath); 21 | this.checkFile(filepath); 22 | 23 | Object.defineProperty(context, 'writer', { 24 | value: { 25 | writeFile: function (path) { 26 | return { 27 | write: function (data) { 28 | fs.appendFileSync(path, data); 29 | }, 30 | println: function (data) { 31 | fs.appendFileSync(path, data + '\r\n'); 32 | }, 33 | close: function () {}, 34 | colorize: function(string) { 35 | return string; 36 | } 37 | }; 38 | } 39 | } 40 | }); 41 | return context; 42 | }, 43 | checkDirectory (dirpath) { 44 | let succeed = true; 45 | // Create folder 46 | try { 47 | const stat = fs.statSync(dirpath); 48 | } catch (e) { 49 | succeed = false; 50 | Log.info('Creating a new folder', dirpath); 51 | try { 52 | mkdirp.sync(dirpath); 53 | } catch (e) { 54 | console.error('Something went wrong while creating folder', e, e.stack); 55 | } 56 | } 57 | return succeed; 58 | }, 59 | checkFile (filepath) { 60 | let succeed = true; 61 | // Reset file 62 | try { 63 | Log.info('Try to remove the content & create the file', filepath); 64 | fs.writeFileSync(filepath, ''); 65 | } catch (e) { 66 | succeed = false; 67 | console.error('Something went wrong while creating the file', filepath, e, e.stack); 68 | } 69 | return succeed; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /server/report/report-coverage.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export default class { 4 | constructor(res, options) { 5 | this.res = res; 6 | this.options = options; 7 | this.options.filename = this.options.path; 8 | } 9 | 10 | generate() { 11 | let coverage = Core.getCoverageObject(); 12 | var coverageReport = JSON.stringify(coverage), 13 | reportPath = this.options.path; 14 | let instance = this; 15 | fs.writeFile(reportPath, coverageReport, function (err) { 16 | /* istanbul ignore else */ 17 | if (err) { 18 | instance.res.end(JSON.stringify({ type: 'failed', message: 'failed to write report file: ' + reportPath })); 19 | } else { 20 | instance.res.end('{"type":"success"}'); 21 | } 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/report/report-generic.js: -------------------------------------------------------------------------------- 1 | import CoverageData from './../services/coverage-data'; 2 | import Core from './../services/core'; 3 | import ReportCommon from './report-common'; 4 | import Conf from '../context/conf'; 5 | const ReportImpl = Npm.require('istanbul-reports'); 6 | 7 | /** 8 | * Used by type lcovonly and json 9 | * create the corresponding file using istanbul api 10 | * @type {any} 11 | */ 12 | export default class { 13 | constructor(res, type, options) { 14 | this.res = res; 15 | this.options = options; 16 | this.report = ReportImpl.create(type, this.options); 17 | this.report.file = this.options.path; 18 | this.context = ReportCommon.getContext(this.report.file); 19 | } 20 | 21 | generate() { 22 | const coverage = Core.getCoverageObject(); 23 | let childs = CoverageData.getLcovonlyReport(coverage); 24 | this.report.onStart(null, this.context); 25 | /* istanbul ignore else */ 26 | if (childs.length === 0) { 27 | this.res.setHeader('Content-type', 'text/plain'); 28 | this.res.statusCode = 500; 29 | return this.res.end('{"type":"No coverage to export"}'); 30 | } 31 | 32 | this.writeFile(childs); 33 | this.res.end('{"type":"success"}'); 34 | } 35 | 36 | writeFile(childs) { 37 | for (let i = 0; i < childs.length; i++) { 38 | // Remove the COVERAGE_APP_FOLDER from the filepath 39 | childs[i].fileCoverage.data.path = childs[i].fileCoverage.data.path.replace(Conf.COVERAGE_APP_FOLDER, ''); 40 | 41 | this.report.onDetail(childs[i]); 42 | } 43 | this.report.onEnd(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /server/report/report-html.js: -------------------------------------------------------------------------------- 1 | import CoverageData from '../services/coverage-data'; 2 | import Core from '../services/core'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import ReportCommon from './report-common'; 6 | import Log from './../context/log'; 7 | const Report = Npm.require('istanbul-lib-report'), 8 | ReportImpl = Npm.require('istanbul-reports'); 9 | 10 | export default class { 11 | constructor(res, options) { 12 | this.res = res; 13 | this.options = options; 14 | this.prefix = options.prefix; 15 | this.options.subdir = this.options.path; 16 | this.opts = this.generateOpts(); 17 | this.report = ReportImpl.create('html', this.opts); 18 | } 19 | 20 | generateOpts() { 21 | const outputPath = this.options.path; 22 | return { 23 | verbose: this.options.verbose, 24 | linkMapper: { 25 | getPath: function (node) { 26 | /* istanbul ignore else */ 27 | if (typeof node === 'string') { 28 | return node; 29 | } 30 | var filePath = node.getQualifiedName(); 31 | 32 | if (node.isSummary()) { 33 | filePath = path.join(outputPath, 'index.html'); 34 | } else { 35 | filePath = path.join(outputPath, filePath + '.html'); 36 | } 37 | return filePath; 38 | }, 39 | relativePath: function (source, target) { 40 | return this.getPath(target); 41 | }, 42 | 43 | assetPath: function (node, name) { 44 | return path.join(outputPath, name); 45 | } 46 | } 47 | }; 48 | } 49 | 50 | generate() { 51 | const folderPath = this.options.path; 52 | this.copyStatic(); 53 | var coverage = Core.getCoverageObject(); 54 | 55 | /* istanbul ignore else */ 56 | if (!(coverage && Object.keys(coverage).length > 0)) { 57 | this.res.statusCode = 500; 58 | return this.res.end('{"type":"failed", "message": "No coverage information have been collected"}'); 59 | } 60 | var root = CoverageData.getTreeReport(coverage); 61 | let filepath = path.join(folderPath, 'index.html'); 62 | 63 | this.report.onSummary(root, ReportCommon.getContext(filepath)); 64 | 65 | const childrens = root.getChildren(); 66 | const report = this.report; 67 | // Todo : use future 68 | childrens.forEach(function (child) { 69 | var filepath = path.join(folderPath, child.getRelativeName() + '.html'); 70 | Log.info('Creating a new html report', filepath); 71 | let fileReport = CoverageData.getFileReport(coverage, child.getRelativeName()); 72 | report.onDetail(fileReport, ReportCommon.getContext(filepath)); 73 | }); 74 | this.res.end('{"type":"success"}'); 75 | } 76 | 77 | copyStatic() { 78 | ReportCommon.checkDirectory(this.options.path); 79 | this.report.onStart(null, this.getFolderContext(this.options.path)); 80 | } 81 | 82 | 83 | getFolderContext(folderpath) { 84 | var context = Report.createContext(); 85 | Object.defineProperty(context, 'writer', { 86 | value: { 87 | copyFile: function (sourcePath, destPath) { 88 | // fix no asset while using test runner 89 | // do not use async - nothing is awaiting us 90 | const data = fs.readFileSync(sourcePath); 91 | let p = path.join(folderpath, destPath); 92 | fs.writeFileSync(p, data); 93 | } 94 | } 95 | }); 96 | return context; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/report/report-http.js: -------------------------------------------------------------------------------- 1 | import CoverageData from '../services/coverage-data'; 2 | import Conf from '../context/conf'; 3 | import Core from '../services/core'; 4 | // If we change Npm.require('istanbul-reports') into import a from 'istanbul-reports' 5 | // the __dirname change and the istanbul dependency fails 6 | // See istanbul-reports 7 | // With Npm.require : /Users/Leo/Webstorm/meteor-container/packages/meteor-coverage/.npm/package/node_modules/istanbul-reports/lib/json 8 | 9 | const Report = Npm.require('istanbul-lib-report'), 10 | ReportImpl = Npm.require('istanbul-reports'); 11 | export default class { 12 | constructor(res, options) { 13 | this.res = res; 14 | this.filepath = ''; 15 | this.options = options; 16 | this.options.prefix = '/coverage/'; 17 | this.opts = this.createOpts(); 18 | } 19 | 20 | createOpts() { 21 | const prefix = this.options.prefix; 22 | return { 23 | verbose: Conf.IS_COVERAGE_VERBOSE, 24 | linkMapper: { 25 | getPath: function (node) { 26 | /* istanbul ignore else */ 27 | if (typeof node === 'string') { 28 | return node; 29 | } 30 | return node.getQualifiedName(); 31 | }, 32 | relativePath: function (source, target) { 33 | return prefix + 'show?p=' + this.getPath(target); 34 | }, 35 | assetPath: function (node, name) { 36 | return prefix + 'asset/' + name; 37 | } 38 | } 39 | }; 40 | } 41 | 42 | generate() { 43 | var coverage = Core.getCoverageObject(); 44 | /* istanbul ignore else */ 45 | if (!(coverage && Object.keys(coverage).length > 0)) { 46 | this.res.setHeader('Content-type', 'text/plain'); 47 | return this.res.end('No coverage information has been collected'); 48 | } 49 | this.res.setHeader('Content-type', 'text/html'); 50 | this.alterFS(this.res); 51 | var context = this.getContext(this.res); 52 | var report = ReportImpl.create('html', this.opts); 53 | if (this.options.filepath) { 54 | var child = CoverageData.getFileReport(coverage, this.options.filepath); 55 | report.onDetail(child, context); 56 | } else { 57 | var root = CoverageData.getTreeReport(coverage); 58 | report.onSummary(root, context); 59 | } 60 | } 61 | 62 | getContext(res) { 63 | var context = Report.createContext(); 64 | Object.defineProperty(context, 'writer', { 65 | value: { 66 | writerForDir: { 67 | writeFile: function () { 68 | return res; 69 | } 70 | }, 71 | writeFile: function () { 72 | return res; 73 | } 74 | } 75 | }); 76 | return context; 77 | } 78 | 79 | // istanbul-reports expect to save HTML report to the file system and not over network 80 | alterFS(res) { 81 | res.close = function () { 82 | this.end(); 83 | }; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /server/report/report-json-summary.js: -------------------------------------------------------------------------------- 1 | import Conf from './../context/conf'; 2 | import CoverageData from './../services/coverage-data'; 3 | import Core from './../services/core'; 4 | import ReportCommon from './report-common'; 5 | const ReportImpl = Npm.require('istanbul-reports'); 6 | 7 | export default class { 8 | constructor(res, type, options) { 9 | this.res = res; 10 | this.options = options; 11 | /* istanbul ignore next: ternary operator */ 12 | this.options.verbose = Conf.IS_COVERAGE_VERBOSE ? true : false; 13 | this.report = ReportImpl.create(type, this.options); 14 | 15 | this.report.file = this.options.path; 16 | this.context = ReportCommon.getContext(this.report.file); 17 | } 18 | 19 | generate() { 20 | const coverage = Core.getCoverageObject(); 21 | let childs = CoverageData.getLcovonlyReport(coverage); 22 | this.report.onStart(null, this.context); 23 | /* istanbul ignore else */ 24 | if (childs.length === 0) { 25 | this.res.setHeader('Content-type', 'text/plain'); 26 | this.res.statusCode = 500; 27 | return this.res.end('{"type":"No coverage to export"}'); 28 | } 29 | this.writeFile(childs); 30 | this.res.end('{"type":"success"}'); 31 | } 32 | 33 | writeFile (childs) { 34 | for (let i = 0; i < childs.length; i++) { 35 | // Remove the COVERAGE_APP_FOLDER from the filepath 36 | childs[i].fileCoverage.data.path = childs[i].fileCoverage.data.path.replace(Conf.COVERAGE_APP_FOLDER, ''); 37 | 38 | this.report.onDetail(childs[i]); 39 | } 40 | ///Todo: not working 41 | //this.report.onSummary(childs); 42 | this.report.onEnd(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /server/report/report-remap.js: -------------------------------------------------------------------------------- 1 | import Conf from './../context/conf'; 2 | import Log from './../context/log'; 3 | import ReportCommon from './report-common'; 4 | import IstanbulGenericReporter from './report-generic'; 5 | import path from 'path'; 6 | const remapIstanbul = Npm.require('remap-istanbul'); 7 | const MemoryStore = Npm.require('istanbul/lib/store/memory'); 8 | 9 | 10 | export default class { 11 | constructor(res, type, options) { 12 | this.res = res; 13 | 14 | // Common options 15 | this.options = options; 16 | 17 | // JSON report options 18 | this.pathJSON = path.join(this.options.path, 'summary.json'); 19 | 20 | // remap-istanbul options 21 | this.remapFolder = path.join(Conf.COVERAGE_EXPORT_FOLDER, '.remap'); 22 | this.remapPath = path.join(Conf.COVERAGE_APP_FOLDER, this.remapFolder); 23 | } 24 | 25 | generateJSONReport() { 26 | const jsonOptions = Object.assign({}, this.options, {path: this.pathJSON}); 27 | let jsonReport = new IstanbulGenericReporter(this.res, 'json', jsonOptions); 28 | jsonReport.generate(); 29 | } 30 | 31 | getFilePath(filename) { 32 | return path.join(this.remapFolder, filename); 33 | } 34 | 35 | generate() { 36 | // We cannot rely on a previous coverage analysis JSON report, 37 | // so we force its generation here before remapping 38 | this.generateJSONReport(); 39 | 40 | const cwd = process.cwd(); 41 | process.chdir(Conf.COVERAGE_APP_FOLDER); 42 | 43 | // Create output directory if not exists 44 | ReportCommon.checkDirectory(this.remapPath); 45 | 46 | let reports = {}, allReports = { 47 | 'html': this.remapPath, 48 | 'clover': this.getFilePath('clover.xml'), 49 | 'cobertura': this.getFilePath('cobertura.xml'), 50 | 'teamcity': this.getFilePath('teamcity.log'), 51 | 'text-summary': this.getFilePath('summary.txt'), 52 | 'text': this.getFilePath('report.txt'), 53 | 'lcovonly': this.getFilePath('lcov.info'), 54 | 'json-summary': this.getFilePath('summary.json'), 55 | 'json': this.getFilePath('report.json') 56 | }; 57 | Conf.remapFormat.forEach((type) => reports[type] = allReports[type]); 58 | this.remapWrapper(this.pathJSON, reports, this.options).await(); 59 | 60 | // Restore previous working directory 61 | process.chdir(cwd); 62 | } 63 | 64 | remapWrapper(sources, reports, options) { 65 | let sourceStore = new MemoryStore(); 66 | let collector = remapIstanbul.remap(remapIstanbul.loadCoverage(sources), { 67 | sources: sourceStore, 68 | warn: function() {} 69 | }); 70 | 71 | /* istanbul ignore else */ 72 | if (!Object.keys(sourceStore.map).length) { 73 | sourceStore = undefined; 74 | } 75 | 76 | let p = Object.keys(reports).map((reportType) => { 77 | let reportOptions = Object.assign({}, this.options, {verbose: reportType === 'html' ? false : true}); 78 | return remapIstanbul.writeReport(collector, reportType, reportOptions, reports[reportType], sourceStore); 79 | }); 80 | 81 | return Promise.all(p); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /server/report/report-service.js: -------------------------------------------------------------------------------- 1 | import Log from './../context/log'; 2 | import Conf from './../context/conf'; 3 | import IstanbulGenericReporter from './report-generic'; 4 | import JsonSummary from './report-json-summary'; 5 | // import Teamcity from './report-teamcity'; 6 | import Html from './report-html'; 7 | import Http from './report-http'; 8 | import ReportCoverage from './report-coverage'; 9 | import ReportRemap from './report-remap'; 10 | import TextSummary from './report-text-summary'; 11 | import path from 'path'; 12 | 13 | export default class { 14 | generateReport(res, type, options) { 15 | 16 | options = Object.assign({}, { 17 | path: path.join(Conf.COVERAGE_APP_FOLDER, Conf.COVERAGE_EXPORT_FOLDER), 18 | /* istanbul ignore next: ternary operator */ 19 | verbose: Log.COVERAGE_VERBOSE ? true : false 20 | }, options); 21 | 22 | Log.info('export coverage using the following format [', type, '] options [', options, ']'); 23 | try { 24 | switch (type) { 25 | case 'remap': 26 | { 27 | let reportRemap = new ReportRemap(res, type, options); 28 | reportRemap.generate(); 29 | break; 30 | } 31 | case 'lcovonly': 32 | { 33 | options = this.addFileToOptions(options, 'lcov.info'); 34 | let istanbulFile1 = new IstanbulGenericReporter(res, type, options); 35 | istanbulFile1.generate(); 36 | break; 37 | } 38 | case 'json': 39 | { 40 | options = this.addFileToOptions(options, 'summary.json'); 41 | let istanbulFile2 = new IstanbulGenericReporter(res, type, options); 42 | istanbulFile2.generate(); 43 | break; 44 | } 45 | case 'coverage': 46 | { 47 | options = this.addFileToOptions(options, 'report.json'); 48 | let reportCoverage = new ReportCoverage(res, options); 49 | reportCoverage.generate(); 50 | break; 51 | } 52 | /*case 'teamcity': 53 | { 54 | options = this.addFileToOptions(options, 'teamcity.log'); 55 | let teamcity = new Teamcity(res, options); 56 | teamcity.generate(); 57 | break; 58 | }*/ 59 | case 'json-summary': 60 | { 61 | options = this.addFileToOptions(options, 'json_summary.json'); 62 | let jsonSummary = new JsonSummary(res, type, options); 63 | jsonSummary.generate(); 64 | break; 65 | } 66 | case 'html': 67 | { 68 | options = Object.assign({}, { 69 | 'prefix': '/coverage/' 70 | }, options); 71 | let html = new Html(res, options); 72 | html.generate(); 73 | break; 74 | } 75 | case 'text-summary': 76 | { 77 | options = this.addFileToOptions(options, 'summary.txt'); 78 | let textSummary = new TextSummary(res, type, options); 79 | textSummary.generate(); 80 | break; 81 | } 82 | case 'http': 83 | { 84 | let http = new Http(res, options); 85 | http.generate(); 86 | break; 87 | } 88 | default: 89 | { 90 | Log.error('Failed to export - this type is not implemented yet'); 91 | res.writeHead(400); 92 | res.end('{"type":"This type [' + type + '] is not supported"}'); 93 | } 94 | } 95 | } catch (e) { 96 | Log.error('ReportService failed while creating report type [', type, ']'); 97 | console.error(e, e.stack); 98 | res.writeHead(400); 99 | res.end('{"type":"error","message":"Unexpected error"}'); 100 | } 101 | } 102 | addFileToOptions(options, filename) { 103 | return Object.assign({}, options, { 104 | path: path.join(options.path, filename) 105 | }); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /server/report/report-teamcity.js: -------------------------------------------------------------------------------- 1 | import Conf from './../context/conf'; 2 | import CoverageData from './../services/coverage-data'; 3 | import Core from './../services/core'; 4 | import ReportCommon from './report-common'; 5 | 6 | const ReportImpl = Npm.require('istanbul-reports'); 7 | 8 | export default class { 9 | constructor(res, options) { 10 | this.res = res; 11 | this.options = options; 12 | this.report = ReportImpl.create(type, this.options); 13 | this.report.file = this.options.path; 14 | this.context = ReportCommon.getContext(this.report.file); 15 | } 16 | 17 | generate() { 18 | let coverage = Core.getCoverageObject(); 19 | var childs = CoverageData.getLcovonlyReport(coverage); 20 | 21 | if (childs.length === 0) { 22 | this.res.setHeader('Content-type', 'text/plain'); 23 | this.res.statusCode = 500; 24 | return this.res.end('{"type":"No coverage to export"}'); 25 | } 26 | 27 | this.writeFile(childs); 28 | this.res.end('{"type":"success"}'); 29 | } 30 | 31 | writeFile (childs) { 32 | for (var i = 0; i < childs.length; i++) { 33 | // Remove the COVERAGE_APP_FOLDER from the filepath 34 | childs[i].fileCoverage.data.path = childs[i].fileCoverage.data.path.replace(Conf.COVERAGE_APP_FOLDER, ''); 35 | } 36 | this.report.onStart(childs, this.context); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /server/report/report-text-summary.js: -------------------------------------------------------------------------------- 1 | import Conf from '../context/conf'; 2 | import CoverageData from '../services/coverage-data'; 3 | import Core from '../services/core'; 4 | import ReportCommon from './report-common'; 5 | import path from 'path'; 6 | import fs from 'fs'; 7 | var Report = Npm.require('istanbul-lib-report'), 8 | ReportImpl = Npm.require('istanbul-reports'); 9 | 10 | export default class { 11 | constructor(res, type, options) { 12 | this.res = res; 13 | this.options = options; 14 | this.report = ReportImpl.create(type, this.options); 15 | 16 | this.report.file = this.options.path; 17 | this.context = this.getContext(this.report.file); 18 | 19 | } 20 | 21 | generate() { 22 | let coverage = Core.getCoverageObject(); 23 | var root = CoverageData.getTreeReport(coverage); 24 | this.report.onStart(root, this.context); 25 | this.res.end('{"type":"success"}'); 26 | } 27 | 28 | getContext(filepath) { 29 | const dirpath = path.dirname(filepath); 30 | ReportCommon.checkDirectory(dirpath); 31 | ReportCommon.checkFile(filepath); 32 | var context = Report.createContext(); 33 | 34 | 35 | Object.defineProperty(context, 'writer', { 36 | value: { 37 | writeFile: function (path) { 38 | return { 39 | write: function (data) { 40 | fs.appendFileSync(path, data); 41 | }, 42 | println: function (data) { 43 | fs.appendFileSync(path, data + '\r\n'); 44 | }, 45 | close: function () { 46 | }, 47 | colorize: function(string) { 48 | return string; 49 | } 50 | }; 51 | } 52 | } 53 | }); 54 | return context; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/router.js: -------------------------------------------------------------------------------- 1 | import Handlers from './handlers'; 2 | import Conf from './context/conf'; 3 | import bodyParser from 'body-parser'; 4 | import url from 'url'; 5 | 6 | const handleRequest = (method) => (path, cb) => { 7 | WebApp.rawConnectHandlers.use(path, (req, res, next) => { 8 | if (req.method !== method) { 9 | next(); 10 | return; 11 | } 12 | 13 | const queryString = url.parse(req.url).query || ''; 14 | const queryParams = { query: {} }; 15 | queryString.split('&').forEach((pair) => { 16 | queryParams.query[pair.split('=')[0]] = pair.split('=')[1]; 17 | }); 18 | 19 | Promise.resolve() 20 | .then(() => new Promise(resolve => { 21 | bodyParser.urlencoded({ extended: false })(req, res, resolve); 22 | })) 23 | .then(() => new Promise(resolve => { 24 | bodyParser.json({ limit: '30mb' }).call(null, req, res, resolve); 25 | })) 26 | .then(() => cb(queryParams, req, res, next)) 27 | .catch((e) => { 28 | console.log('Exception undandled:'); 29 | console.log(e.stack); 30 | 31 | next(); 32 | }); 33 | }); 34 | }; 35 | 36 | export default class { 37 | constructor() { 38 | if (Conf.IS_COVERAGE_ACTIVE) { 39 | this.bindRoutes(); 40 | } 41 | } 42 | 43 | bindRoutes() { 44 | // Show static assets 45 | handleRequest('GET')('/coverage/asset', (params, req, res, next) => { 46 | params.filename = url.parse(req.url).path.match(/(\/([^\/]+))?/)[2]; 47 | Handlers.getAsset(params, req, res, next); 48 | }); 49 | 50 | // export coverage to file 51 | handleRequest('GET')('/coverage/export', (params, req, res, next) => { 52 | params.type = url.parse(req.url).path.match(/(\/([^\/]+))?/)[2]; 53 | Handlers.exportFile(params, req, res, next); 54 | }); 55 | 56 | handleRequest('GET')('/coverage/import', Handlers.importCoverage); 57 | 58 | // merge client coverage posted from browser 59 | handleRequest('POST')('/coverage/client', Handlers.addClientCoverage); 60 | 61 | handleRequest('GET')('/coverage', Handlers.showCoverage); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /server/services/core.js: -------------------------------------------------------------------------------- 1 | import Conf from './../context/conf'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | const Coverage = Npm.require('istanbul-lib-coverage'); 5 | 6 | let mergeCoverageWith, importCoverage, getCoverageObject; 7 | 8 | getCoverageObject = function () { 9 | /* istanbul ignore next: default assignment */ 10 | global.__coverage__ = global.__coverage__ || {}; 11 | return global.__coverage__; 12 | }; 13 | 14 | setCoverageObject = function (obj) { 15 | global.__coverage__ = obj; 16 | }; 17 | 18 | mergeCoverageWith = function (obj) { 19 | /* istanbul ignore else */ 20 | if (!obj) { 21 | return; 22 | } 23 | var coverage = getCoverageObject(); 24 | var coverageMap = Coverage.createCoverageMap(coverage); 25 | coverageMap.addFileCoverage(obj); 26 | setCoverageObject(coverageMap.toJSON()); 27 | }; 28 | 29 | 30 | /* istanbul ignore next: default assignment */ 31 | importCoverage = function (res, options = {}) { 32 | Log.info('import coverage'); 33 | /* istanbul ignore next: ternary operator */ 34 | const filename = options.filename ? options.filename : 'report.json'; 35 | const reportPath = path.join(Conf.COVERAGE_APP_FOLDER, Conf.COVERAGE_EXPORT_FOLDER, filename); 36 | fs.exists(reportPath, function (exists) { 37 | /* istanbul ignore else */ 38 | if (!exists) { 39 | res.end(JSON.stringify({ type: 'failed', message: 'report file not found: reportPath=' + reportPath + ' COVERAGE_APP_FOLDER=' + Conf.COVERAGE_APP_FOLDER })); 40 | return; 41 | } 42 | fs.readFile(reportPath, 'utf8', function (err, fileContent) { 43 | /* istanbul ignore else */ 44 | if (err) { 45 | res.end(JSON.stringify({ type: 'failed', message: 'failed to read report file: ' + reportPath })); 46 | return; 47 | } 48 | let coverageObj = JSON.parse(fileContent); 49 | for (let property in coverageObj) { 50 | /* istanbul ignore else */ 51 | if (coverageObj.hasOwnProperty(property)) { 52 | Core.mergeCoverageWith(coverageObj[property]); 53 | } 54 | } 55 | res.end('{"type":"success"}'); 56 | }); 57 | }); 58 | }; 59 | export default Core = { 60 | mergeCoverageWith, 61 | importCoverage, 62 | getCoverageObject 63 | }; 64 | -------------------------------------------------------------------------------- /server/services/coverage-data.js: -------------------------------------------------------------------------------- 1 | import Conf from './../context/conf'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | const Coverage = Npm.require('istanbul-lib-coverage'); 6 | const Report = Npm.require('istanbul-lib-report'); 7 | 8 | export default CoverageData = { 9 | getReport: function (coverage) { 10 | if (Package['lmieulet:meteor-legacy-coverage'] && Package['lmieulet:meteor-legacy-coverage'].default && Package['lmieulet:meteor-legacy-coverage'].default.CoverageData) { 11 | // Retrieve the coverage report from the other lib, as we used the legacy system 12 | return Package['lmieulet:meteor-legacy-coverage'].default.CoverageData.getReport(coverage); 13 | } else if (Meteor.isPackageTest) { 14 | // MANDATORY FOR PACKAGES TESTS 15 | throw new Error('lmieulet:meteor-legacy-coverage not found. Just add this server dependency in Package.onTest in your package.js'); 16 | } 17 | // Used for meteor apps that relies on babel 18 | return Coverage.createCoverageMap(coverage); 19 | }, 20 | getFileReport: function (coverage, filePath) { 21 | const coverageMap = this.getReport(coverage); 22 | const node = Report.summarizers.flat(coverageMap); 23 | const childs = node.getRoot().getChildren(); 24 | let child; 25 | for (let i = 0; i < childs.length; i++) { 26 | /* istanbul ignore else */ 27 | if (childs[i].getRelativeName() === filePath) { 28 | child = childs[i]; 29 | // fix the path if possible 30 | if (child && child.fileCoverage && 31 | child.fileCoverage.data && child.fileCoverage.data.path && 32 | child.fileCoverage.data.path.indexOf(Conf.COVERAGE_APP_FOLDER)) { 33 | // add the folder in the path if not present 34 | child.fileCoverage.data.path = path.join(Conf.COVERAGE_APP_FOLDER, child.fileCoverage.data.path); 35 | } 36 | } 37 | } 38 | return child; 39 | }, 40 | getTreeReport: function (coverage) { 41 | return this.getNodeReport(coverage).getRoot(); 42 | }, 43 | getLcovonlyReport: function (coverage) { 44 | return this.getTreeReport(coverage).getChildren(); 45 | }, 46 | getNodeReport: function (coverage) { 47 | const coverageMap = this.getReport(coverage); 48 | return Report.summarizers.flat(coverageMap); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /server/tests.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import meteorCoverageApi from 'meteor/lmieulet:meteor-coverage'; 3 | 4 | const CoverageData = meteorCoverageApi.CoverageData; 5 | const Router = meteorCoverageApi.Router; 6 | const ReportService = meteorCoverageApi.ReportService; 7 | const Conf = meteorCoverageApi.Conf; 8 | 9 | 10 | describe('meteor coverage', function () { 11 | 12 | it('is importeable in es6', function () { 13 | assert.isNotNull(CoverageData); 14 | assert.isNotNull(Router); 15 | assert.isNotNull(ReportService); 16 | assert.isNotNull(Conf); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /someapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "someapp", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run" 6 | }, 7 | "dependencies": { 8 | "@babel/runtime": "^7.1.5", 9 | "meteor-node-stubs": "^0.4.1", 10 | "puppeteer": "^1.12.2" 11 | } 12 | } 13 | --------------------------------------------------------------------------------