coverage.js

const mongoose = require('mongoose');

/**
 * @class Coverage
 * @property {string} run_at - the iso date when the coverage was send to the server
 * @property {Object[]} source_files - a list of all the source files related to the coverage report
 * @property {string} source_files[].name - the name of the file parsed
 * @property {string} source_files[].source - the content of the file parsed into a string
 * @property {Object[]} source_files[].coverage - an array of objects describing the state of the parsed file
 * @property {object} source_files[].coverage[].lines - an object descibing the lines covered in the file
 * @property {number} source_files[].coverage[].lines.found - total number of lines found in the file
 * @property {number} source_files[].coverage[].lines.hits - total number of lines covered in the file
 * @property {Object[]} source_files[].coverage[].lines.details - an array of points in the file that descibes the line and the amount of times it was covered
 * @property {number} source_files[].coverage[].lines.details.line - the line number that is covered
 * @property {number} source_files[].coverage[].lines.details.hit - how many times the line was hit
 * @property {object} source_files[].coverage[].functions - an object descibing the functions covered in the file
 * @property {number} source_files[].coverage[].functions.found - total number of functions found in the file
 * @property {number} source_files[].coverage[].functions.hits - total number of functions covered in the file
 * @property {Object[]} source_files[].coverage[].functions.details - an array of points in the file that descibes the line and the amount of times it was covered
 * @property {number} source_files[].coverage[].functions.details.line - the line number that is covered
 * @property {number} source_files[].coverage[].functions.details.hit - how many times the line was hit
 * @property {object} source_files[].coverage[].branches - an object descibing the branches covered in the file
 * @property {number} source_files[].coverage[].branches.found - total number of branches found in the file
 * @property {number} source_files[].coverage[].branches.hits - total number of branches covered in the file
 * @property {Object[]} source_files[].coverage[].branches.details - an array of points in the file that descibes the line and the amount of times it was covered
 * @property {number} source_files[].coverage[].branches.details.line - the line number that is covered
 * @property {number} source_files[].coverage[].branches.details.hit - how many times the line was hit
 * @property {object} git - the state of the git config at the time of sending coverage
 * @property {object} git.head - git head details
 * @property {string} git.head.id - the commit id
 * @property {string} git.head.committer_name - the committer name
 * @property {string} git.head.committer_email - the committer email
 * @property {string} git.head.message - the commit message
 * @property {string} git.head.author_name - the author of the commit's name
 * @property {string} git.head.author_email - the author of the commit's email
 * @property {string} git.head.branch - the current working branch
 * @property {Object[]} git.head.remotes - an array of all the remotes
 * @property {string} git.head.remotes[].name - the name of the remote
 * @property {string} git.head.remotes[].url - the url of the remote
 * @example
 *
 *{
 *   "source_files": [{
 *       "name": "util/lcov.js",
 *       "source": "var fs = require('fs'),\n    path = require('path');\n\nvar exists = fs.exists || path.exists;\n\nvar walkFile = function(str, cb) {\n    var data = [], item;\n\n    [ 'end_of_record' ].concat(str.split('\\n')).forEach(function(line) {\n        line = line.trim();\n        var allparts = line.split(':'),\n            parts = [allparts.shift(), allparts.join(':')],\n            lines, fn;\n\n        switch (parts[0].toUpperCase()) {\n            case 'TN':\n                item.title = parts[1].trim();\n                break;\n            case 'SF':\n                item.file = parts.slice(1).join(':').trim();\n                break;\n            case 'FNF':\n                item.functions.found = Number(parts[1].trim());\n                break;\n            case 'FNH':\n                item.functions.hit = Number(parts[1].trim());\n                break;\n            case 'LF':\n                item.lines.found = Number(parts[1].trim());\n                break;\n            case 'LH':\n                item.lines.hit = Number(parts[1].trim());\n                break;\n            case 'DA':\n                lines = parts[1].split(',');\n                item.lines.details.push({\n                    line: Number(lines[0]),\n                    hit: Number(lines[1])\n                });\n                break;\n            case 'FN':\n                fn = parts[1].split(',');\n                item.functions.details.push({\n                    name: fn[1],\n                    line: Number(fn[0])\n                });\n                break;\n            case 'FNDA':\n                fn = parts[1].split(',');\n                item.functions.details.some(function(i, k) {\n                    if (i.name === fn[1] && i.hit === undefined) {\n                        item.functions.details[k].hit = Number(fn[0]);\n                        return true;\n                    }\n                });\n                break;\n            case 'BRDA':\n                fn = parts[1].split(',');\n                item.branches.details.push({\n                    line: Number(fn[0]),\n                    block: Number(fn[1]),\n                    branch: Number(fn[2]),\n                    taken: ((fn[3] === '-') ? 0 : Number(fn[3]))\n                });\n                break;\n            case 'BRF':\n                item.branches.found = Number(parts[1]);\n                break;\n            case 'BRH':\n                item.branches.hit = Number(parts[1]);\n                break;\n        }\n\n        if (line.indexOf('end_of_record') > -1) {\n            data.push(item);\n            item = {\n              lines: {\n                  found: 0,\n                  hit: 0,\n                  details: []\n              },\n              functions: {\n                  hit: 0,\n                  found: 0,\n                  details: []\n              },\n              branches: {\n                hit: 0,\n                found: 0,\n                details: []\n              }\n            };\n        }\n    });\n\n    data.shift();\n\n    if (data.length) {\n        cb(undefined, data);\n    } else {\n        cb('Failed to parse string');\n    }\n};\n\nmodule.exports.parse = function(file, cb) {\n    exists(file, function(x) {\n        if (!x) {\n            return walkFile(file, cb);\n        }\n        fs.readFile(file, 'utf8', function(err, str) {\n            walkFile(str, cb);\n        });\n    });\n\n};\n",
 *       "coverage": [{
 *           "lines": {
 *               "found": 53,
 *               "hit": 53,
 *               "details": [{
 *                   "line": 1,
 *                   "hit": 1
 *               }, {...}]
 *           },
 *           "functions": {
 *               "hit": 6,
 *               "found": 6,
 *               "details": [{
 *                   "name": "(anonymous_0)",
 *                   "line": 7,
 *                   "hit": 3
 *               }, {...}]
 *           },
 *           "branches": {
 *               "hit": 24,
 *               "found": 24,
 *               "details": [{
 *                   "line": 16,
 *                   "block": 0,
 *                   "branch": 0,
 *                   "taken": 4
 *               }, {...}]
 *           },
 *           "title": "",
 *           "source": "",
 *           "file": "/Users/gacsapo/Documents/temp/node-coverage-server/util/lcov.js"
 *       }]
 *   }],
 *   "git": {
 *       "head": {
 *           "id": "07e4ee9f38d7c41fed09a2b93f6ce23c4a2c49da",
 *           "committer_name": "Gabriel Csapo",
 *           "committer_email": "gabecsapo@gmail.com",
 *           "message": "Initial commit",
 *           "author_name": "Gabriel Csapo",
 *           "author_email": "gabecsapo@gmail.com"
 *       },
 *       "branch": "master",
 *       "remotes": [{
 *           "name": "origin",
 *           "url": "https://github.com/gabrielcsapo/node-coverage-server.git"
 *       }]
 *   },
 *   "run_at": "2017-01-17T23:18:16.248Z"
 *}
 **/

const CoverageModel = new mongoose.Schema({}, { _id: false, strict: false });
const Coverage = mongoose.model('coverages', CoverageModel);

module.exports = {
    /**
     * saves a coverage model the collection
     * @function save
     * @memberof Coverage
     * @param  {Coverage} model - the coverage model
     * @return {Promise} - a promise that resolves with the model after it was inserted
     */
    save: (model) => {
      return new Promise((resolve, reject) => {
          const { git } = model;
          const _id = `${git.commit}#${git.branch}`;

          model['_id'] = _id;
          Coverage.findOneAndUpdate({
              _id
          }, model, { upsert: true }, (err, result) => {
              if(err) { return reject(err); }
              return resolve(result);
          });
      });
    },
    /**
     * gets a repos coverage model or all coverage models
     * @function get
     * @memberof Coverage
     * @param  {string=} repo - the url of the repo
     * @return {Coverage[]} - a promise that resolves with the model after it was inserted
     */
    get: (repo) => {
        return new Promise((resolve, reject) => {
            const options = [{
                $group: {
                    _id: "$git.remotes.url",
                    history: {
                        $push: "$$ROOT"
                    }
                }
            }];

            if(repo)
                options.unshift({ $match: { "git.remotes.url": repo} });

            Coverage.aggregate(options, (err, docs) => {
                if(err) { return reject(err); }
                return resolve(docs);
            });
        });
    }
};