Browse files

Initial commit.

  • Loading branch information...
0 parents commit e585cd3b6ed024edce08c1333274745f99631f84 @hitsthings committed Feb 4, 2013
Showing with 510 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +1 −0 .npmignore
  3. +22 −0 LICENSE-MIT
  4. +49 −0 README.md
  5. +19 −0 bin/istanbul-proxy
  6. +39 −0 grunt.js
  7. +1 −0 index.js
  8. +26 −0 lib/istanbul-proxy-client.js
  9. +268 −0 lib/istanbul-proxy.js
  10. +5 −0 lib/send-report-client.js
  11. +45 −0 package.json
  12. +34 −0 test/istanbul-proxy_test.js
1 .gitignore
@@ -0,0 +1 @@
+node_modules/
1 .npmignore
@@ -0,0 +1 @@
+/node_modules/
22 LICENSE-MIT
@@ -0,0 +1,22 @@
+Copyright (c) 2013 Adam Ahmed
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
49 README.md
@@ -0,0 +1,49 @@
+# istanbul-proxy
+
+Run Istanbul coverage on JS in the browser through an HTTP proxy
+
+## Getting Started
+
+1. Install the module with: `npm install -g istanbul-proxy`
+2. Run istanbul-proxy
+3. Set your browser up to use the local port that istanbul-proxy is running on as a proxy server.
+4. Hit the urls you want to get coverage for
+5. Visit the istanbul-proxy server directly to view coverage reports (or view the static files in the reportDir).
+
+```
+> istanbul-proxy --help
+
+ Usage: istanbul-proxy [options]
+
+ Options:
+
+ -h, --help output usage information
+ -V, --version output the version number
+ -p, --port [port] The HTTP port to listen on
+ -r, --reportDir [path] The directory in which to write HTML report
+ing files.
+ -t, --reportingTimeout [millis] How long after window.onload the coverage
+ report should be reported to the server. If
+ set to 0, coverage will not be reported.
+ Your pages must then call
+ istanbulProxy.sendReport() when finished.
+ -n, --passThroughUrls [urls] URLs that should not be instrumented
+```
+
+## Examples
+
+```
+> istanbul-proxy -p 6984 -r C:\Data\proxy-test
+HTML reporting files will be stored in C:\Data\proxy-test
+Proxy server running on port 6984
+```
+
+## Contributing
+In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [grunt](https://github.com/gruntjs/grunt).
+
+## Release History
+0.1.0 - Initial release.
+
+## License
+Copyright (c) 2013 Adam Ahmed
+Licensed under the MIT license.
19 bin/istanbul-proxy
@@ -0,0 +1,19 @@
+#!/usr/env/node
+var program = require('commander');
+
+program
+ .version(require('../package.json').version)
+ .option('-p, --port [port]', 'The HTTP port to listen on')
+ .option('-r, --reportDir [path]', 'The directory in which to write HTML reporting files.')
+ .option('-t, --reportingTimeout [millis]', 'How long after window.onload the coverage report should be reported to the server. If set to 0, coverage will not be reported. Your pages must then call istanbulProxy.sendReport() when finished.')
+ .option('-n, --passThroughUrls [urls]', 'URLs that should not be instrumented')
+ .parse(process.argv);
+
+require('../index')({
+ port : program.port,
+ reportDir : program.reportDir,
+ reportingTimeout : program.reportingTimeout,
+ passThroughUrls : typeof program.passThroughUrls === 'string' ?
+ program.passThroughUrls.split(/\s+/g) :
+ program.passThroughUrls
+});
39 grunt.js
@@ -0,0 +1,39 @@
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ pkg: '<json:package.json>',
+ test: {
+ files: ['test/**/*.js']
+ },
+ lint: {
+ files: ['grunt.js', 'lib/**/*.js', 'test/**/*.js']
+ },
+ watch: {
+ files: '<config:lint.files>',
+ tasks: 'default'
+ },
+ jshint: {
+ options: {
+ curly: true,
+ eqeqeq: true,
+ immed: true,
+ latedef: true,
+ newcap: true,
+ noarg: true,
+ sub: true,
+ undef: true,
+ boss: true,
+ eqnull: true,
+ node: true
+ },
+ globals: {
+ exports: true
+ }
+ }
+ });
+
+ // Default task.
+ grunt.registerTask('default', 'lint test');
+
+};
1 index.js
@@ -0,0 +1 @@
+module.exports = require('./lib/istanbul-proxy');
26 lib/istanbul-proxy-client.js
@@ -0,0 +1,26 @@
+var istanbulProxy = (function() {
+
+ var reportId = new Date().getTime();
+
+ function sendReport() {
+ if (window.XMLHttpRequest) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "/istanbul", true);
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState != 4) return;
+ if (xhr.status != 200) return console.log('Failed to report coverage. Status code: ' + xhr.status);
+ return console.log('Coverage reported.');
+ };
+ xhr.send(JSON.stringify({
+ coverage: window.__coverage__ || null,
+ url : location.href
+ }));
+ } else {
+ window.console && console.log && console.log("XMLHttpRequest required to send coverage report.");
+ }
+ }
+
+ return {
+ sendReport : sendReport
+ };
+}());
268 lib/istanbul-proxy.js
@@ -0,0 +1,268 @@
+var http = require('http');
+var fs = require('fs');
+var path = require('path');
+var url = require('url');
+var querystring = require('querystring');
+var os = require('os');
+var dns = require('dns');
+
+var connect = require('connect');
+
+var url2path = require('url2path');
+
+var temp = require('temp');
+
+var mkdirp = require('mkdirp');
+
+var istanbul = require('istanbul');
+var Instrumenter = istanbul.Instrumenter;
+var Collector = istanbul.Collector;
+var Report = istanbul.Report;
+var Store = istanbul.Store;
+
+
+function isMyIp(ip) {
+ var networkInterfaces = os.networkInterfaces();
+ return Object.keys(networkInterfaces).some(function(interfaceType) {
+ var interfacesOfType = networkInterfaces[interfaceType];
+ return interfacesOfType && interfacesOfType.some(function(networkInterface) {
+ return networkInterface.address === ip;
+ });
+ });
+}
+
+var hostnameCache = {};
+function isMyHostname(hostname, cb) {
+ if (hostnameCache[hostname]) {
+ return cb(null, hostnameCache[hostname]);
+ }
+ dns.lookup(hostname, function(err, address) {
+ if (err) return cb(err);
+ return cb(null, hostnameCache[hostname] = isMyIp(address));
+ });
+}
+
+function isType(mimeRE, urlRE) {
+ return function(req, res) {
+ if (res.headers['content-type']) {
+ return mimeRE.test(res.headers['content-type']);
+ }
+ return urlRE.test(req.url);
+ };
+}
+var isJs = isType(/javascript/, /\.js\s*$/);
+var isHtml = isType(/html/, /\.(htm(l?)|asp(x?)|php|jsp)\s*$/);
+
+var istanbulClientScript;
+var sendReportScript;
+function getReportingScript(timeout) {
+ if (!istanbulClientScript) {
+ istanbulClientScript = fs.readFileSync( path.join(__dirname, 'istanbul-proxy-client.js'), 'utf8' );
+ }
+
+ if (!timeout) {
+ return istanbulClientScript;
+ }
+
+ if (!sendReportScript) {
+ sendReportScript = fs.readFileSync( path.join(__dirname, 'send-report-client.js'), 'utf8' );
+ }
+
+ return istanbulClientScript + sendReportScript.replace('%TIMEOUT%', timeout);
+}
+
+function insertReporter(htmlSource, reportingTimeout) {
+ // insert before first script, or before the end of head, or just at the top
+ var insertMatch = /<script>|<\/head>/.exec(htmlSource);
+ var insertIndex = insertMatch && insertMatch.index || 0;
+
+ return htmlSource.substring(0, insertIndex) +
+ "<script>" + getReportingScript(reportingTimeout) + "</script>" +
+ htmlSource.substring(insertIndex);
+}
+
+function proxy(req, res, getProxyResponseTransform) {
+ var options = url.parse(req.url);
+ options.headers = req.headers;
+ options.method = req.method;
+
+ var proxy_req = http.request(options, (getProxyResponseTransform || cleanResponse)(req, res));
+ req.on('data', function(chunk) {
+ proxy_req.write(chunk, 'binary');
+ });
+ req.on('end', function() {
+ proxy_req.end();
+ });
+}
+
+function instrumentationResponse(options, req, res) {
+ return function (proxy_res) {
+ var needsReport = isHtml(req, proxy_res);
+ var needsInstrumentation = ~options.instrumentUrls.indexOf(req.url) || isJs(req, proxy_res);
+ var responseStr = "";
+
+ proxy_res.on('data', needsReport || needsInstrumentation ?
+ function(chunk) { responseStr += chunk; } :
+ function(chunk) { res.write(chunk, 'binary'); }
+ );
+
+ proxy_res.on('end', function() {
+ if (needsInstrumentation) {
+ res.write(instrument(responseStr, req.url, options));
+ }
+ if (needsReport) {
+ res.write(options.reportingTimeout && insertReporter(responseStr, options.reportingTimeout));
+ }
+ res.end();
+ });
+ res.writeHead(proxy_res.statusCode, proxy_res.headers);
+ };
+}
+
+function instrument(source, sourceUrl, options) {
+ // Generate a valid filepath from the url
+ var filepath = url2path.url2pathRelative(sourceUrl);
+ options.sourceStore.set(filepath, source);
+ return options.instrumenter.instrumentSync(source, filepath);
+}
+
+function proxyThroughInstrumentation(req, res, options) {
+ return proxy(req, res, instrumentationResponse.bind(null, options));
+}
+
+function cleanResponse(req, res) {
+ return function (proxy_res) {
+ proxy_res.on('data', function(chunk) { res.write(chunk, 'binary'); });
+ proxy_res.on('end', res.end.bind(res));
+ res.writeHead(proxy_res.statusCode, proxy_res.headers);
+ };
+}
+
+module.exports = function(options) {
+ options = options || {};
+
+ var instrumenter = options.instrumenter = new Instrumenter({
+ embedSource : true // we only have URLs to work with, so can't get the source from disk.
+ });
+ var collector = options.collector = new Collector();
+ var sourceStore = options.sourceStore = Store.create('memory');
+
+ var port = options.port = parseInt(options.port || 8080, 10);
+ var reportDir = options.reportDir = options.reportDir || temp.mkdirSync();
+ var reportingTimeout = options.reportingTimeout = options.reportingTimeout === undefined ?
+ 1000 :
+ parseInt(options.reportingTimeout || 0, 10);
+
+ // These urls are used by the istanbul reporter, and shouldn't be instrumented.
+ var passThroughUrls = options.passThroughUrls = [
+ 'http://yui.yahooapis.com/3.6.0/build/yui/yui-min.js',
+ 'http://yui.yahooapis.com/combo?3.6.0/build/widget-uievents/widget-uievents-min.js&3.6.0/build/datatable-base/datatable-base-min.js&3.6.0/build/datatable-column-widths/datatable-column-widths-min.js&3.6.0/build/intl/intl-min.js&3.6.0/build/datatable-message/lang/datatable-message_en.js&3.6.0/build/datatable-message/datatable-message-min.js&3.6.0/build/datatable-mutable/datatable-mutable-min.js&3.6.0/build/datatable-sort/lang/datatable-sort_en.js&3.6.0/build/datatable-sort/datatable-sort-min.js&3.6.0/build/plugin/plugin-min.js&3.6.0/build/datasource-local/datasource-local-min.js&3.6.0/build/datatable-datasource/datatable-datasource-min.js',
+ 'http://yui.yahooapis.com/combo?3.6.0/build/node-core/node-core-min.js&3.6.0/build/node-base/node-base-min.js&3.6.0/build/event-base/event-base-min.js&3.6.0/build/event-delegate/event-delegate-min.js&3.6.0/build/node-event-delegate/node-event-delegate-min.js&3.6.0/build/datatable-core/datatable-core-min.js&3.6.0/build/view/view-min.js&3.6.0/build/classnamemanager/classnamemanager-min.js&3.6.0/build/datatable-head/datatable-head-min.js&3.6.0/build/datatable-body/datatable-body-min.js&3.6.0/build/datatable-table/datatable-table-min.js&3.6.0/build/pluginhost-base/pluginhost-base-min.js&3.6.0/build/pluginhost-config/pluginhost-config-min.js&3.6.0/build/base-pluginhost/base-pluginhost-min.js&3.6.0/build/event-synthetic/event-synthetic-min.js&3.6.0/build/event-focus/event-focus-min.js&3.6.0/build/dom-style/dom-style-min.js&3.6.0/build/node-style/node-style-min.js&3.6.0/build/widget-base/widget-base-min.js&3.6.0/build/widget-htmlparser/widget-htmlparser-min.js&3.6.0/build/widget-skin/widget-skin-min.js',
+ 'http://yui.yahooapis.com/combo?3.6.0/build/escape/escape-min.js&3.6.0/build/array-extras/array-extras-min.js&3.6.0/build/array-invoke/array-invoke-min.js&3.6.0/build/arraylist/arraylist-min.js&3.6.0/build/attribute-core/attribute-core-min.js&3.6.0/build/base-core/base-core-min.js&3.6.0/build/oop/oop-min.js&3.6.0/build/event-custom-base/event-custom-base-min.js&3.6.0/build/event-custom-complex/event-custom-complex-min.js&3.6.0/build/attribute-events/attribute-events-min.js&3.6.0/build/attribute-extras/attribute-extras-min.js&3.6.0/build/attribute-base/attribute-base-min.js&3.6.0/build/attribute-complex/attribute-complex-min.js&3.6.0/build/base-base/base-base-min.js&3.6.0/build/base-build/base-build-min.js&3.6.0/build/json-parse/json-parse-min.js&3.6.0/build/model/model-min.js&3.6.0/build/model-list/model-list-min.js&3.6.0/build/dom-core/dom-core-min.js&3.6.0/build/dom-base/dom-base-min.js&3.6.0/build/selector-native/selector-native-min.js&3.6.0/build/selector/selector-min.js'
+ ].concat(options.passThroughUrls || []);
+ // These urls may not match the mime type or URL regex, but should be instrumented anyway
+ var instrumentUrls = options.instrumentUrls = options.instrumentUrls || [];
+
+
+
+ var isDirty = true;
+ function reportCoverage(coverageJSON, coverageUrl) {
+ // the "testName" parameter isn't currently used by istanbul, but passing in a value
+ // anyway in case it gets used in the future.
+ collector.add(coverageJSON, coverageUrl);
+ isDirty = true;
+ }
+
+ function writeReport(cb) {
+ if (isDirty) {
+ Report.create('html', {
+ dir : reportDir,
+ sourceStore: sourceStore,
+ verbose: false
+ }).writeReport(collector, !!'sync');
+ }
+ isDirty = false;
+ cb && cb();
+ }
+
+ var staticServer;
+ mkdirp(reportDir, function() {
+ staticServer = connect.static(reportDir);
+ });
+ function viewReport(req, res) {
+ writeReport(function() {
+ staticServer(req, res, function(err) {
+ if (err) return handleErr(res, e, 500);
+ res.writeHead(404);
+ res.end();
+ });
+ });
+ }
+
+ function handleOwnRequest(req, res) {
+ viewReport(req, res);
+ }
+
+ function handleUserError(res, e) {
+ handleErr(res, e, 400);
+ }
+ function handleErr(res, e, status) {
+ console.log(e);
+ res.writeHead(status || 400, { 'content-type' : 'text/plain' });
+ res.write(e && e.getMessage ? e.getMessage() : e);
+ res.end();
+ }
+
+ function handleKnownHostRequest(isOwnRequest, req, res) {
+ if (isOwnRequest) {
+ handleOwnRequest(req, res);
+ } else if (req.method === 'POST' && url.parse(req.url).pathname === '/istanbul') {
+ var reqBody = '';
+ req.on('data', function(chunk) { reqBody += chunk; });
+ req.on('end', function() {
+ var json;
+ try {
+ json = JSON.parse(reqBody);
+ if (json) {
+ reportCoverage(json.coverage, json.url);
+ }
+ } catch (e) {
+ return handleUserError(res, e);
+ }
+ res.writeHead(200);
+ res.end();
+ });
+ } else {
+ proxyThroughInstrumentation(req, res, options);
+ }
+ }
+
+ http.createServer(function(req, res) {
+ console.log(req.url);
+ if (~passThroughUrls.indexOf(req.url)) {
+ return proxy(req, res);
+ }
+
+ var hostAndPort = req.headers['host'].split(':');
+ var parsedPort = parseInt(hostAndPort[1] || 0, 10);
+ var isMyPort = parsedPort === port || (port === 80 && !parsedPort);
+ if (!isMyPort) {
+ return handleKnownHostRequest(false, req, res);
+ }
+ isMyHostname(hostAndPort[0], function(err, isMyHostname) {
+ if (err) {
+ console.log(err);
+ res.writeHead(500);
+ res.end();
+ return;
+ }
+ handleKnownHostRequest(isMyHostname, req, res);
+ });
+ }).listen(port, function() {
+ console.log('Proxy server running on port ' + port);
+ });
+ console.log('HTML reporting files will be stored in ' + reportDir);
+ if (!reportingTimeout) {
+ console.log('Coverage is not being automatically reported to the server. Call istanbulProxy.sendReport() manually.');
+ }
+};
5 lib/send-report-client.js
@@ -0,0 +1,5 @@
+window.onload = function() {
+ setTimeout(function() {
+ istanbulProxy.sendReport();
+ }, %TIMEOUT%);
+};
45 package.json
@@ -0,0 +1,45 @@
+{
+ "name": "istanbul-proxy",
+ "description": "Run Istanbul coverage on JS in the browser through an HTTP proxy",
+ "version": "0.1.0",
+ "homepage": "https://github.com/hitsthings/istanbul-proxy",
+ "author": {
+ "name": "Adam Ahmed",
+ "email": "hitsthings@gmail.com",
+ "url": "http://noiregrets.com"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/hitsthings/istanbul-proxy.git"
+ },
+ "bugs": {
+ "url": "https://github.com/hitsthings/istanbul-proxy/issues"
+ },
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "https://github.com/hitsthings/istanbul-proxy/blob/master/LICENSE-MIT"
+ }
+ ],
+ "main": "index",
+ "bin": {
+ "istanbul-proxy": "bin/istanbul-proxy"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ },
+ "scripts": {
+ "test": "grunt test"
+ },
+ "dependencies" : {
+ "istanbul" : "latest",
+ "temp" : "latest",
+ "connect" : "latest",
+ "url2path" : "latest",
+ "mkdirp" : ">=0.3.3"
+ },
+ "//devDependencies": {
+ "grunt": "~0.3.17"
+ },
+ "keywords": []
+}
34 test/istanbul-proxy_test.js
@@ -0,0 +1,34 @@
+var istanbul_proxy = require('../lib/istanbul-proxy.js');
+
+/*
+ ======== A Handy Little Nodeunit Reference ========
+ https://github.com/caolan/nodeunit
+
+ Test methods:
+ test.expect(numAssertions)
+ test.done()
+ Test assertions:
+ test.ok(value, [message])
+ test.equal(actual, expected, [message])
+ test.notEqual(actual, expected, [message])
+ test.deepEqual(actual, expected, [message])
+ test.notDeepEqual(actual, expected, [message])
+ test.strictEqual(actual, expected, [message])
+ test.notStrictEqual(actual, expected, [message])
+ test.throws(block, [error], [message])
+ test.doesNotThrow(block, [error], [message])
+ test.ifError(value)
+*/
+
+exports['awesome'] = {
+ setUp: function(done) {
+ // setup here
+ done();
+ },
+ 'no args': function(test) {
+ test.expect(1);
+ // tests here
+ test.equal(istanbul_proxy.awesome(), 'awesome', 'should be awesome.');
+ test.done();
+ }
+};

0 comments on commit e585cd3

Please sign in to comment.