Skip to content

Instantly share code, notes, and snippets.

@dubbha
Forked from robertknight/istanbul.md
Last active January 22, 2022 17:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dubbha/a67e498fc23b6dc57d52f7117df10456 to your computer and use it in GitHub Desktop.
Save dubbha/a67e498fc23b6dc57d52f7117df10456 to your computer and use it in GitHub Desktop.
How Istanbul works

Istanbul Notes

These are some notes I made while reviewing hypothesis/client#156 to understand how Istanbul works

Istanbul instruments code in order to generate code coverage metrics for tests by adding code to record lines, statements etc. that are executed.

It adds a global __coverage__ variable to the generated code which is a map from file path to coverage information. The code for each module is then augmented with:

  • A header which creates an entry in the __coverage__ map containing a set of hit counters for functions, branches and statements and mappings of functions/branches/statements back to their locations in the original source

  • Statements added around each statement or expression from the original code which increment the appropriate hit counter just before a function, branch or statement is executed.

See the coverage.json docs for details on the format of the __coverage__ map.

Inspecting the instrumented code

When using Karma + Browserify, the generated code is written to a ".browserify" file in the "/tmp" directory. When running the tests in "watch" mode using "gulp test-watch", the name of this bundle is printed whenever a source file is changed after the first run of the tests.

As long as the test runner is running, you can open this file in a text editor to see what the generated code looks like with instrumentation added.

Example, foo.coffee:

class Foo
  constructor: (arg) ->
    console.log 'Foo Coffee', Foo.toString()
    if arg
      @arg = arg
    else
      throw new Error('arg missing')

module.exports = Foo

Instrumented code, using the "isparta" instrumenter:

[function(require,module,exports){

var __cov_LmQeoDnoeGOnxVJ9q82G5Q = (Function('return this'))();
if (!__cov_LmQeoDnoeGOnxVJ9q82G5Q.__coverage__) { __cov_LmQeoDnoeGOnxVJ9q82G5Q.__coverage__ = {}; }
__cov_LmQeoDnoeGOnxVJ9q82G5Q = __cov_LmQeoDnoeGOnxVJ9q82G5Q.__coverage__;
if (!(__cov_LmQeoDnoeGOnxVJ9q82G5Q['/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee'])) {
   __cov_LmQeoDnoeGOnxVJ9q82G5Q['/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee'] = {"path":"/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee","s":{"1":0,"2":0,"3":1,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0},"b":{"1":[0,0]},"f":{"1":0,"2":0},"fnMap":{"1":{"name":"(anonymous_1)","line":3,"loc":{"start":{"line":1,"column":-9},"end":{"line":1,"column":-9}}},"2":{"name":"Foo","line":4,"loc":{"start":{"line":2,"column":15},"end":{"line":2,"column":15}}}},"statementMap":{"1":{"start":{"line":1,"column":-15},"end":{"line":1,"column":-15}},"2":{"start":{"line":0,"column":0},"end":{"line":0,"column":0},"skip":true},"3":{"start":{"line":0,"column":0},"end":{"line":0,"column":0},"skip":true},"4":{"start":{"line":3,"column":4},"end":{"line":3,"column":4}},"5":{"start":{"line":4,"column":4},"end":{"line":2,"column":15}},"6":{"start":{"line":5,"column":6},"end":{"line":4,"column":4}},"7":{"start":{"line":0,"column":0},"end":{"line":0,"column":0},"skip":true},"8":{"start":{"line":0,"column":0},"end":{"line":0,"column":0},"skip":true},"9":{"start":{"line":9,"column":0},"end":{"line":9,"column":17}}},"branchMap":{"1":{"line":6,"type":"if","locations":[{"start":{"line":4,"column":4},"end":{"line":4,"column":4}},{"start":{"line":4,"column":4},"end":{"line":4,"column":4}}]}}};
}
__cov_LmQeoDnoeGOnxVJ9q82G5Q = __cov_LmQeoDnoeGOnxVJ9q82G5Q['/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee'];
__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['1']++;var Foo;__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['2']++;Foo=function(){__cov_LmQeoDnoeGOnxVJ9q82G5Q.f['1']++;function Foo(arg){__cov_LmQeoDnoeGOnxVJ9q82G5Q.f['2']++;__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['4']++;console.log('Foo Coffee',Foo.toString());__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['5']++;if(arg){__cov_LmQeoDnoeGOnxVJ9q82G5Q.b['1'][0]++;__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['6']++;this.arg=arg;}else{__cov_LmQeoDnoeGOnxVJ9q82G5Q.b['1'][1]++;__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['7']++;throw new Error('arg missing');}}__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['8']++;return Foo;}();__cov_LmQeoDnoeGOnxVJ9q82G5Q.s['9']++;module.exports=Foo;

},{}]

Prettifid version of the __coverage__ object:

{
  "path": "/home/robert/hypothesis/repos/client/h/static/scripts/annotator/foo.coffee",
  "s": {
    "1": 0,
    "2": 0,
    "3": 1,
    "4": 0,
    "5": 0,
    "6": 0,
    "7": 0,
    "8": 0,
    "9": 0
  },
  "b": {
    "1": [
      0,
      0
    ]
  },
  "f": {
    "1": 0,
    "2": 0
  },
  "fnMap": {
    "1": {
      "name": "(anonymous_1)",
      "line": 3,
      "loc": {
        "start": {
          "line": 1,
          "column": -9
        },
        "end": {
          "line": 1,
          "column": -9
        }
      }
    },
    "2": {
      "name": "Foo",
      "line": 4,
      "loc": {
        "start": {
          "line": 2,
          "column": 15
        },
        "end": {
          "line": 2,
          "column": 15
        }
      }
    }
  },
  "statementMap": {
    "1": {
      "start": {
        "line": 1,
        "column": -15
      },
      "end": {
        "line": 1,
        "column": -15
      }
    },
    "2": {
      "start": {
        "line": 0,
        "column": 0
      },
      "end": {
        "line": 0,
        "column": 0
      },
      "skip": true
    },
    "3": {
      "start": {
        "line": 0,
        "column": 0
      },
      "end": {
        "line": 0,
        "column": 0
      },
      "skip": true
    },
    "4": {
      "start": {
        "line": 3,
        "column": 4
      },
      "end": {
        "line": 3,
        "column": 4
      }
    },
    "5": {
      "start": {
        "line": 4,
        "column": 4
      },
      "end": {
        "line": 2,
        "column": 15
      }
    },
    "6": {
      "start": {
        "line": 5,
        "column": 6
      },
      "end": {
        "line": 4,
        "column": 4
      }
    },
    "7": {
      "start": {
        "line": 0,
        "column": 0
      },
      "end": {
        "line": 0,
        "column": 0
      },
      "skip": true
    },
    "8": {
      "start": {
        "line": 0,
        "column": 0
      },
      "end": {
        "line": 0,
        "column": 0
      },
      "skip": true
    },
    "9": {
      "start": {
        "line": 9,
        "column": 0
      },
      "end": {
        "line": 9,
        "column": 17
      }
    }
  },
  "branchMap": {
    "1": {
      "line": 6,
      "type": "if",
      "locations": [
        {
          "start": {
            "line": 4,
            "column": 4
          },
          "end": {
            "line": 4,
            "column": 4
          }
        },
        {
          "start": {
            "line": 4,
            "column": 4
          },
          "end": {
            "line": 4,
            "column": 4
          }
        }
      ]
    }
  }
}

Prettified version of the instrumented code:

__cov__.s['1']++;
var Foo;
__cov__.s['2']++;
Foo = function() {
    __cov__.f['1']++;

    function Foo(arg) {
        __cov__.f['2']++;
        __cov__.s['4']++;
        console.log('Foo Coffee', Foo.toString());
        __cov__.s['5']++;
        if (arg) {
            __cov__.b['1'][0]++;
            __cov__.s['6']++;
            this.arg = arg;
        } else {
            __cov__.b['1'][1]++;
            __cov__.s['7']++;
            throw new Error('arg missing');
        }
    }
    __cov__.s['8']++;
    return Foo;
}();
__cov__.s['9']++;

Handling compiled/transpiled JavaScript

Istanbul allows overriding the instrumenter which gets fed the original code and outputs the instrumented code. The default instrumenter is replaced with isparta to generate correct function/source/statement location maps for CoffeeScript files, where the location in the generated code is different from the original code. Isparta was originally written for use with the Babel ES2015+ transpiler.

Without using "isparta", the generated maps from function/statement/branch number to original source file and line refer to the location in the JS code output by the CoffeeScript compiler. With "isparta", the generated maps refer to the location in the original CoffeeScript code.

Even though "isparta" is not a CoffeeScript processing tool, it works by subclassing the Istanbul instrumenter and overriding functions that transform the statement/function/branch location maps. The transformers convert locations in the generated code back to locations in the original code by reading the source maps. These source-maps are generated in the same format by all transpilers (Babel, CoffeeScript, TypeScript etc.) so it enables code coverage reporting for CoffeeScript even though it was designed for use with Babel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment