jacoco.js

/**
 * @module lib/jacoco
 */

const fs = require('fs');
const { parseString } = require('xml2js');

function getCounter(source, type) {
  return source.counter.filter(function(counter) {
    return counter.$.type === type;
  })[0];
}

function walkFile(xml, cb) {
  parseString(xml, function(err, parseResult) {
    if (err) {
      return cb('Failed to parse coverage');
    }

    var packages = parseResult.report.package;
    var output = [];

    packages.forEach(function(pack) {
        var cov = pack.sourcefile.map(function(s) {
        var fullPath = pack.$.name + '/' + s.$.name;
        var className = fullPath.substring(0, fullPath.lastIndexOf('.'));

        var c = pack.class.filter(function(cl) {
          return cl.$.name === className;
        })[0];

        var methods = getCounter(s, "METHOD");
        var lines = getCounter(s, "LINE");
        var branches = getCounter(s, "BRANCH");
        if (!branches) {
          branches = {
            $: {
              covered: 0,
              missed: 0
            }
          };
        }
        var classCov = {
          title: s.$.name,
          file: fullPath,
          functions: {
            found: Number(methods.$.covered) + Number(methods.$.missed),
            hit: Number(methods.$.covered),
            details: !c.method ? [] : c.method.map(function(m) {
              var hit = m.counter.some(function(counter) {
                return counter.$.type === "METHOD" && counter.$.covered === "1";
              });
              return {
                name: m.$.name,
                line: Number(m.$.line),
                hit: hit ? 1 : 0
              };
            })
          },
          lines: {
            found: Number(lines.$.covered) + Number(lines.$.missed),
            hit: Number(lines.$.covered),
            details: !s.line ? [] : s.line.map(function(l) {
              return {
                line: Number(l.$.nr),
                hit: Number(l.$.ci)
              };
            })
          },
          branches: {
            found: Number(branches.$.covered) + Number(branches.$.missed),
            hit: Number(branches.$.covered),
            details: !s.line ? [] : [].concat.apply([],
              s.line.filter(function(l) {
                return Number(l.$.mb) > 0 || Number(l.$.cb) > 0;
              })
              .map(function(l) {
                var branches = [];
                var count = Number(l.$.mb) + Number(l.$.cb);

                for (var i = 0; i < count; ++i) {
                  branches = branches.concat({
                    line: Number(l.$.nr),
                    block: 0,
                    branch: Number(i),
                    taken: i < Number(l.$.cb) ? 1 : 0
                  });
                }

                return branches;
              })
            )
          }
        };

        return classCov;
      });

      output = output.concat(cov);
    });

    cb(null, output);
  });
}

/**
 * returns a javascript object that represents the coverage data
 * @method parse
 * @param  {String|Path} file - this can either be a string or a path to a file
 * @return {Coverage} - The coverage data structure
 *
 */
function parse(file) {
  return new Promise(function(resolve, reject) {
    if (fs.existsSync(file)) {
      fs.readFile(file, 'utf8', (err, str) => {
        if (err) {
          reject(err);
        }
        return walkFile(str, function(err, result) {
          if (err) return reject(err);
          resolve(result);
        });
      });
    } else {
      return walkFile(file, function(err, result) {
        if (err) return reject(err);
        resolve(result);
      });
    }
  });
}

module.exports = {
  walkFile,
  parse
};