[Pkg-javascript-commits] [node-tap-mocha-reporter] 02/137: stuff mostly working

Bastien Roucariès rouca at moszumanska.debian.org
Thu Sep 7 09:49:21 UTC 2017


This is an automated email from the git hooks/post-receive script.

rouca pushed a commit to branch master
in repository node-tap-mocha-reporter.

commit 30b92354cb5497d994b7e8916f8330b5ee281bab
Author: isaacs <i at izs.me>
Date:   Fri Apr 10 14:47:18 2015 -0700

    stuff mostly working
---
 index.js                            |  69 ++++
 lib/browser/debug.js                |   4 +
 lib/browser/escape-string-regexp.js |  11 +
 lib/browser/events.js               | 177 +++++++++
 lib/browser/fs.js                   |   0
 lib/browser/glob.js                 |   0
 lib/browser/path.js                 |   0
 lib/browser/progress.js             | 125 +++++++
 lib/browser/tty.js                  |  12 +
 lib/formatter.js                    |  78 ++++
 lib/ms.js                           | 109 ++++++
 lib/reporters/base.js               |   8 +-
 lib/reporters/dump.js               |  35 ++
 lib/reporters/index.js              |  36 +-
 lib/reporters/silent.js             |   1 +
 lib/runner.js                       | 241 +++++++++++++
 lib/suite.js                        |  21 ++
 lib/test.js                         |  28 ++
 lib/utils.js                        | 698 ++++++++++++++++++++++++++++++++++++
 package.json                        |  24 ++
 20 files changed, 1657 insertions(+), 20 deletions(-)

diff --git a/index.js b/index.js
new file mode 100644
index 0000000..a2ccd42
--- /dev/null
+++ b/index.js
@@ -0,0 +1,69 @@
+#!/usr/bin/env node
+
+module.exports = Formatter
+
+var util = require('util')
+var reporters = require('./lib/reporters/index.js')
+var Writable = require('stream').Writable
+var Runner = require('./lib/runner.js')
+
+util.inherits(Formatter, Writable)
+
+function Formatter (type, options) {
+  if (!reporters[type]) {
+    console.error('Unknown format type: %s\n\n%s', type, avail())
+    type = 'silent'
+  }
+
+  var runner = this.runner = new Runner(options)
+  this.reporter = new reporters[type](this.runner)
+  Writable.call(this, options)
+
+  runner.on('end', function () {
+    process.nextTick(function () {
+      if (!runner.parser.ok)
+        process.exit(1)
+    })
+  })
+}
+
+Formatter.prototype.write = function () {
+  return this.runner.write.apply(this.runner, arguments)
+}
+
+Formatter.prototype.end = function () {
+  return this.runner.end.apply(this.runner, arguments)
+}
+
+function avail () {
+  var types = Object.keys(reporters).sort().reduce(function (str, t) {
+    var ll = str.split('\n').pop().length + t.length
+    if (ll < 40)
+      return str + ' ' + t
+    else
+      return str + '\n' + t
+  }, '').trim()
+
+  return 'Available format types:\n\n' + types
+}
+
+
+function usage (err) {
+  console[err ? 'error' : 'log'](function () {/*
+Usage:
+  tap-mocha-reporter <type>
+
+Reads TAP data on stdin, and formats to stdout using the specified
+reporter.  (Note that some reporters write to files instead of stdout.)
+
+%s
+*/}.toString().split('\n').slice(1, -1).join('\n'), avail())
+}
+
+if (require.main === module) {
+  var type = process.argv[2]
+  if (!type)
+    return usage()
+
+  process.stdin.pipe(new Formatter(type))
+}
diff --git a/lib/browser/debug.js b/lib/browser/debug.js
new file mode 100644
index 0000000..0d939e5
--- /dev/null
+++ b/lib/browser/debug.js
@@ -0,0 +1,4 @@
+module.exports = function(type){
+  return function(){
+  }
+};
diff --git a/lib/browser/escape-string-regexp.js b/lib/browser/escape-string-regexp.js
new file mode 100644
index 0000000..21a9566
--- /dev/null
+++ b/lib/browser/escape-string-regexp.js
@@ -0,0 +1,11 @@
+'use strict';
+
+var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
+
+module.exports = function (str) {
+  if (typeof str !== 'string') {
+    throw new TypeError('Expected a string');
+  }
+
+  return str.replace(matchOperatorsRe,  '\\$&');
+};
diff --git a/lib/browser/events.js b/lib/browser/events.js
new file mode 100644
index 0000000..f708260
--- /dev/null
+++ b/lib/browser/events.js
@@ -0,0 +1,177 @@
+/**
+ * Module exports.
+ */
+
+exports.EventEmitter = EventEmitter;
+
+/**
+ * Check if `obj` is an array.
+ */
+
+function isArray(obj) {
+  return '[object Array]' == {}.toString.call(obj);
+}
+
+/**
+ * Event emitter constructor.
+ *
+ * @api public
+ */
+
+function EventEmitter(){};
+
+/**
+ * Adds a listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.on = function (name, fn) {
+  if (!this.$events) {
+    this.$events = {};
+  }
+
+  if (!this.$events[name]) {
+    this.$events[name] = fn;
+  } else if (isArray(this.$events[name])) {
+    this.$events[name].push(fn);
+  } else {
+    this.$events[name] = [this.$events[name], fn];
+  }
+
+  return this;
+};
+
+EventEmitter.prototype.addListener = EventEmitter.prototype.on;
+
+/**
+ * Adds a volatile listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.once = function (name, fn) {
+  var self = this;
+
+  function on () {
+    self.removeListener(name, on);
+    fn.apply(this, arguments);
+  };
+
+  on.listener = fn;
+  this.on(name, on);
+
+  return this;
+};
+
+/**
+ * Removes a listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.removeListener = function (name, fn) {
+  if (this.$events && this.$events[name]) {
+    var list = this.$events[name];
+
+    if (isArray(list)) {
+      var pos = -1;
+
+      for (var i = 0, l = list.length; i < l; i++) {
+        if (list[i] === fn || (list[i].listener && list[i].listener === fn)) {
+          pos = i;
+          break;
+        }
+      }
+
+      if (pos < 0) {
+        return this;
+      }
+
+      list.splice(pos, 1);
+
+      if (!list.length) {
+        delete this.$events[name];
+      }
+    } else if (list === fn || (list.listener && list.listener === fn)) {
+      delete this.$events[name];
+    }
+  }
+
+  return this;
+};
+
+/**
+ * Removes all listeners for an event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.removeAllListeners = function (name) {
+  if (name === undefined) {
+    this.$events = {};
+    return this;
+  }
+
+  if (this.$events && this.$events[name]) {
+    this.$events[name] = null;
+  }
+
+  return this;
+};
+
+/**
+ * Gets all listeners for a certain event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.listeners = function (name) {
+  if (!this.$events) {
+    this.$events = {};
+  }
+
+  if (!this.$events[name]) {
+    this.$events[name] = [];
+  }
+
+  if (!isArray(this.$events[name])) {
+    this.$events[name] = [this.$events[name]];
+  }
+
+  return this.$events[name];
+};
+
+/**
+ * Emits an event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.emit = function (name) {
+  if (!this.$events) {
+    return false;
+  }
+
+  var handler = this.$events[name];
+
+  if (!handler) {
+    return false;
+  }
+
+  var args = [].slice.call(arguments, 1);
+
+  if ('function' == typeof handler) {
+    handler.apply(this, args);
+  } else if (isArray(handler)) {
+    var listeners = handler.slice();
+
+    for (var i = 0, l = listeners.length; i < l; i++) {
+      listeners[i].apply(this, args);
+    }
+  } else {
+    return false;
+  }
+
+  return true;
+};
diff --git a/lib/browser/fs.js b/lib/browser/fs.js
new file mode 100644
index 0000000..e69de29
diff --git a/lib/browser/glob.js b/lib/browser/glob.js
new file mode 100644
index 0000000..e69de29
diff --git a/lib/browser/path.js b/lib/browser/path.js
new file mode 100644
index 0000000..e69de29
diff --git a/lib/browser/progress.js b/lib/browser/progress.js
new file mode 100644
index 0000000..b30e517
--- /dev/null
+++ b/lib/browser/progress.js
@@ -0,0 +1,125 @@
+/**
+ * Expose `Progress`.
+ */
+
+module.exports = Progress;
+
+/**
+ * Initialize a new `Progress` indicator.
+ */
+
+function Progress() {
+  this.percent = 0;
+  this.size(0);
+  this.fontSize(11);
+  this.font('helvetica, arial, sans-serif');
+}
+
+/**
+ * Set progress size to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.size = function(n){
+  this._size = n;
+  return this;
+};
+
+/**
+ * Set text to `str`.
+ *
+ * @param {String} str
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.text = function(str){
+  this._text = str;
+  return this;
+};
+
+/**
+ * Set font size to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.fontSize = function(n){
+  this._fontSize = n;
+  return this;
+};
+
+/**
+ * Set font `family`.
+ *
+ * @param {String} family
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.font = function(family){
+  this._font = family;
+  return this;
+};
+
+/**
+ * Update percentage to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.update = function(n){
+  this.percent = n;
+  return this;
+};
+
+/**
+ * Draw on `ctx`.
+ *
+ * @param {CanvasRenderingContext2d} ctx
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.draw = function(ctx){
+  try {
+    var percent = Math.min(this.percent, 100)
+      , size = this._size
+      , half = size / 2
+      , x = half
+      , y = half
+      , rad = half - 1
+      , fontSize = this._fontSize;
+
+    ctx.font = fontSize + 'px ' + this._font;
+
+    var angle = Math.PI * 2 * (percent / 100);
+    ctx.clearRect(0, 0, size, size);
+
+    // outer circle
+    ctx.strokeStyle = '#9f9f9f';
+    ctx.beginPath();
+    ctx.arc(x, y, rad, 0, angle, false);
+    ctx.stroke();
+
+    // inner circle
+    ctx.strokeStyle = '#eee';
+    ctx.beginPath();
+    ctx.arc(x, y, rad - 1, 0, angle, true);
+    ctx.stroke();
+
+    // text
+    var text = this._text || (percent | 0) + '%'
+      , w = ctx.measureText(text).width;
+
+    ctx.fillText(
+        text
+      , x - w / 2 + 1
+      , y + fontSize / 2 - 1);
+  } catch (ex) {} //don't fail if we can't render progress
+  return this;
+};
diff --git a/lib/browser/tty.js b/lib/browser/tty.js
new file mode 100644
index 0000000..eab6388
--- /dev/null
+++ b/lib/browser/tty.js
@@ -0,0 +1,12 @@
+exports.isatty = function(){
+  return true;
+};
+
+exports.getWindowSize = function(){
+  if ('innerHeight' in global) {
+    return [global.innerHeight, global.innerWidth];
+  } else {
+    // In a Web Worker, the DOM Window is not available.
+    return [640, 480];
+  }
+};
diff --git a/lib/formatter.js b/lib/formatter.js
new file mode 100644
index 0000000..91cd89f
--- /dev/null
+++ b/lib/formatter.js
@@ -0,0 +1,78 @@
+// A formatter is a Duplex stream that TAP data is written into,
+// and then something else (presumably not-TAP) is read from.
+//
+// See tap-classic.js for an example of a formatter in use.
+
+var Duplex = require('stream').Duplex
+var util = require('util')
+var Parser = require('tap-parser')
+util.inherits(Formatter, Duplex)
+module.exports = Formatter
+
+function Formatter(options, parser, parent) {
+  if (!(this instanceof Formatter))
+    return new Formatter(options, parser, parent)
+
+  if (!parser)
+    parser = new Parser()
+
+  Duplex.call(this, options)
+  this.child = null
+  this.parent = parent || null
+  this.level = parser.level
+  this.parser = parser
+
+  attachEvents(this, parser, options)
+
+  if (options.init)
+    options.init.call(this)
+}
+
+function attachEvents (self, parser, options) {
+  var events = [
+    'version', 'plan', 'assert', 'comment',
+    'complete', 'extra', 'bailout'
+  ]
+
+  parser.on('child', function (childparser) {
+    self.child = new Formatter(options, childparser, self)
+    if (options.child)
+      options.child.call(self, self.child)
+  })
+
+  events.forEach(function (ev) {
+    if (typeof options[ev] === 'function')
+      parser.on(ev, options[ev].bind(self))
+  })
+
+  // proxy all stream events directly
+  var streamEvents = [
+    'pipe', 'prefinish', 'finish', 'unpipe', 'close'
+  ]
+
+  streamEvents.forEach(function (ev) {
+    parser.on(ev, function () {
+      var args = [ev]
+      args.push.apply(args, arguments)
+      self.emit.apply(self, args)
+    })
+  })
+}
+
+Formatter.prototype.write = function (c, e, cb) {
+  return this.parser.write(c, e, cb)
+}
+
+Formatter.prototype.end = function (c, e, cb) {
+  return this.parser.end(c, e, cb)
+}
+
+Formatter.prototype._read = function () {}
+
+// child formatters always push data to the root obj
+Formatter.prototype.push = function (c) {
+  if (this.parent)
+    return this.parent.push(c)
+
+  Duplex.prototype.push.call(this, c)
+}
diff --git a/lib/ms.js b/lib/ms.js
new file mode 100644
index 0000000..ba451fa
--- /dev/null
+++ b/lib/ms.js
@@ -0,0 +1,109 @@
+/**
+ * Helpers.
+ */
+
+var s = 1000;
+var m = s * 60;
+var h = m * 60;
+var d = h * 24;
+var y = d * 365.25;
+
+/**
+ * Parse or format the given `val`.
+ *
+ * Options:
+ *
+ *  - `long` verbose formatting [false]
+ *
+ * @param {String|Number} val
+ * @param {Object} options
+ * @return {String|Number}
+ * @api public
+ */
+
+module.exports = function(val, options){
+  options = options || {};
+  if ('string' == typeof val) return parse(val);
+  return options['long'] ? longFormat(val) : shortFormat(val);
+};
+
+/**
+ * Parse the given `str` and return milliseconds.
+ *
+ * @param {String} str
+ * @return {Number}
+ * @api private
+ */
+
+function parse(str) {
+  var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str);
+  if (!match) return;
+  var n = parseFloat(match[1]);
+  var type = (match[2] || 'ms').toLowerCase();
+  switch (type) {
+    case 'years':
+    case 'year':
+    case 'y':
+      return n * y;
+    case 'days':
+    case 'day':
+    case 'd':
+      return n * d;
+    case 'hours':
+    case 'hour':
+    case 'h':
+      return n * h;
+    case 'minutes':
+    case 'minute':
+    case 'm':
+      return n * m;
+    case 'seconds':
+    case 'second':
+    case 's':
+      return n * s;
+    case 'ms':
+      return n;
+  }
+}
+
+/**
+ * Short format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function shortFormat(ms) {
+  if (ms >= d) return Math.round(ms / d) + 'd';
+  if (ms >= h) return Math.round(ms / h) + 'h';
+  if (ms >= m) return Math.round(ms / m) + 'm';
+  if (ms >= s) return Math.round(ms / s) + 's';
+  return ms + 'ms';
+}
+
+/**
+ * Long format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function longFormat(ms) {
+  return plural(ms, d, 'day')
+    || plural(ms, h, 'hour')
+    || plural(ms, m, 'minute')
+    || plural(ms, s, 'second')
+    || ms + ' ms';
+}
+
+/**
+ * Pluralization helper.
+ */
+
+function plural(ms, n, name) {
+  if (ms < n) return;
+  if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;
+  return Math.ceil(ms / n) + ' ' + name + 's';
+}
diff --git a/lib/reporters/base.js b/lib/reporters/base.js
index c253c11..a1429ef 100644
--- a/lib/reporters/base.js
+++ b/lib/reporters/base.js
@@ -168,12 +168,14 @@ exports.list = function(failures){
     var err = test.err
       , message = err.message || ''
       , stack = err.stack || message
-      , index = stack.indexOf(message) + message.length
+
+    var index = stack.indexOf(message) + message.length
       , msg = stack.slice(0, index)
       , actual = err.actual
       , expected = err.expected
       , escape = true;
 
+
     // uncaught
     if (err.uncaught) {
       msg = 'Uncaught ' + msg;
@@ -199,8 +201,8 @@ exports.list = function(failures){
     }
 
     // indent stack trace without msg
-    stack = stack.slice(index ? index + 1 : index)
-      .replace(/^/gm, '  ');
+    stack = utils.stackTraceFilter()(stack.slice(index ? index + 1 : index)
+      .replace(/^/gm, '  '));
 
     console.log(fmt, (i + 1), test.fullTitle(), msg, stack);
   });
diff --git a/lib/reporters/dump.js b/lib/reporters/dump.js
new file mode 100644
index 0000000..10e7e75
--- /dev/null
+++ b/lib/reporters/dump.js
@@ -0,0 +1,35 @@
+exports = module.exports = Dump
+var Base = require('./base')
+  , cursor = Base.cursor
+  , color = Base.color;
+
+function Dump(runner) {
+  Base.call(this, runner);
+
+  var events = [
+    'start',
+    'version',
+    'end',
+    'suite',
+    'suite end',
+    'test',
+    'pending',
+    'pass',
+    'fail',
+    'test end',
+  ];
+
+  var i = process.argv.indexOf('dump')
+  if (i !== -1) {
+    var args = process.argv.slice(i)
+    if (args.length)
+      events = args
+  }
+
+  events.forEach(function (ev) {
+    runner.on(ev, function () {
+      var args = [].concat.apply([ev], arguments)
+      console.log.apply(console, args)
+    })
+  })
+}
diff --git a/lib/reporters/index.js b/lib/reporters/index.js
index 87b76d9..bdde95b 100644
--- a/lib/reporters/index.js
+++ b/lib/reporters/index.js
@@ -1,17 +1,19 @@
-exports.Base = require('./base');
-exports.Dot = require('./dot');
-exports.Doc = require('./doc');
-exports.TAP = require('./tap');
-exports.JSON = require('./json');
-exports.HTML = require('./html');
-exports.List = require('./list');
-exports.Min = require('./min');
-exports.Spec = require('./spec');
-exports.Nyan = require('./nyan');
-exports.XUnit = require('./xunit');
-exports.Markdown = require('./markdown');
-exports.Progress = require('./progress');
-exports.Landing = require('./landing');
-exports.JSONCov = require('./json-cov');
-exports.HTMLCov = require('./html-cov');
-exports.JSONStream = require('./json-stream');
+exports.base = require('./base.js')
+exports.dot = require('./dot.js')
+exports.doc = require('./doc.js')
+exports.tap = require('./tap.js')
+exports.json = require('./json.js')
+exports.html = require('./html.js')
+exports.list = require('./list.js')
+exports.min = require('./min.js')
+exports.spec = require('./spec.js')
+exports.nyan = require('./nyan.js')
+exports.xunit = require('./xunit.js')
+exports.markdown = require('./markdown.js')
+exports.progress = require('./progress.js')
+exports.landing = require('./landing.js')
+exports.jsoncov = require('./json-cov.js')
+exports.htmlcov = require('./html-cov.js')
+exports.jsonstream = require('./json-stream.js')
+exports.dump = require('./dump.js')
+exports.silent = require('./silent.js')
diff --git a/lib/reporters/silent.js b/lib/reporters/silent.js
new file mode 100644
index 0000000..45548da
--- /dev/null
+++ b/lib/reporters/silent.js
@@ -0,0 +1 @@
+exports = module.exports = function () {}
diff --git a/lib/runner.js b/lib/runner.js
new file mode 100644
index 0000000..87c6756
--- /dev/null
+++ b/lib/runner.js
@@ -0,0 +1,241 @@
+// A facade from the tap-parser to the Mocha "Runner" object.
+// Note that pass/fail/suite events need to also mock the "Runnable"
+// objects (either "Suite" or "Test") since these have functions
+// which are called by the formatters.
+
+module.exports = Runner
+
+// relevant events:
+//
+// start()
+//   Start of the top-level test set
+//
+// end()
+//   End of the top-level test set.
+//
+// fail(test, err)
+//   any "not ok" test that is not the trailing test for a suite
+//   of >0 test points.
+//
+// pass(test)
+//   any "ok" test point that is not the trailing test for a suite
+//   of >0 tests
+//
+// pending(test)
+//   Any "todo" test
+//
+// suite(suite)
+//   A suite is a child test with >0 test points.  This is a little bit
+//   tricky, because TAP will provide a "child" event before we know
+//   that it's a "suite".  We see the "# Subtest: name" comment as the
+//   first thing in the subtest.  Then, when we get our first test point,
+//   we know that it's a suite, and can emit the event with the mock suite.
+//
+// suite end(suite)
+//   Emitted when we end the subtest
+//
+// test(test)
+//   Any test point which is not the trailing test for a suite.
+//
+// test end(test)
+//   Emitted immediately after the "test" event because test points are
+//   not async in TAP.
+
+var util = require('util')
+var Test = require('./test.js')
+var Suite = require('./suite.js')
+var Writable = require('stream').Writable
+var Parser = require('tap-parser')
+
+util.inherits(Runner, Writable)
+
+function Runner (options) {
+  if (!(this instanceof Runner))
+    return new Runner(options)
+
+  var parser = this.parser = new Parser(options)
+  this.startTime = new Date()
+
+  attachEvents(this, parser, 0)
+  Writable.call(this, options)
+}
+
+Runner.prototype.write = function () {
+  if (!this.emittedStart) {
+    this.emittedStart = true
+    this.emit('start')
+  }
+
+  return this.parser.write.apply(this.parser, arguments)
+}
+
+Runner.prototype.end = function () {
+  return this.parser.end.apply(this.parser, arguments)
+}
+
+Parser.prototype.fullTitle = function () {
+  if (!this.parent)
+    return this.name || ''
+  else
+    return this.parent.fullTitle() + ' ' + (this.name || '').trim()
+}
+
+function attachEvents (runner, parser, level) {
+  var events = [
+    'version', 'plan', 'assert', 'comment',
+    'complete', 'extra', 'bailout'
+  ]
+
+  parser.runner = runner
+
+  if (level === 0) {
+    parser.on('version', function (v) {
+      runner.emit('version', v)
+    })
+  }
+
+  parser.emittedSuite = false
+  parser.didAssert = false
+  parser.printed = false
+  parser.name = ''
+  parser.doingChild = null
+
+  parser.on('finish', function () {
+    if (!parser.parent)
+      runner.emit('end')
+  })
+
+  parser.on('child', function (child) {
+    //console.log('>>> child')
+    child.parent = parser
+    attachEvents(runner, child, level + 1)
+
+    // if we're in a suite, but we haven't emitted it yet, then we
+    // know that an assert will follow this child, even if there are
+    // no others. That means that we will definitely have a 'suite'
+    // event to emit.
+    emitSuite(this)
+
+    this.didAssert = true
+    this.doingChild = child
+  })
+
+  parser.on('comment', function (c) {
+    if (!this.printed && c.match(/^# Subtest: /)) {
+      c = c.trim().replace(/^# Subtest: /, '')
+      this.name = c
+    }
+  })
+
+  // Just dump all non-parsing stuff to stderr
+  parser.on('extra', function (c) {
+    process.stderr.write(c)
+  })
+
+  parser.on('assert', function (result) {
+    emitSuite(this)
+
+    // no need to print the trailing assert for subtests
+    // we've already emitted a 'suite end' event for this.
+    if (this.doingChild && this.doingChild.didAssert &&
+        this.doingChild.name === result.name) {
+      this.doingChild = null
+      return
+    }
+
+    this.didAssert = true
+    this.doingChild = null
+
+    emitTest(this, result)
+  })
+
+  parser.on('complete', function (results) {
+    this.results = results
+    if (this.suite)
+      runner.emit('suite end', this.suite)
+  })
+
+  // proxy all stream events directly
+  var streamEvents = [
+    'pipe', 'prefinish', 'finish', 'unpipe', 'close'
+  ]
+
+  streamEvents.forEach(function (ev) {
+    parser.on(ev, function () {
+      var args = [ev]
+      args.push.apply(args, arguments)
+      runner.emit.apply(runner, args)
+    })
+  })
+}
+
+function emitSuite (parser) {
+  //console.log('emitSuite', parser.emittedSuite, parser.level, parser.name)
+  if (!parser.emittedSuite && parser.name) {
+    parser.emittedSuite = true
+    var suite = parser.suite = new Suite(parser)
+    if (parser.parent && parser.parent.suite)
+      parser.parent.suite.suites.push(suite)
+    parser.runner.emit('suite', suite)
+  }
+}
+
+function emitTest (parser, result) {
+  var runner = parser.runner
+  var test = new Test(result, parser)
+
+  if (parser.suite) {
+    //if (test.parent === parser)
+    //  test.parent = parser.suite
+    parser.suite.tests.push(test)
+  }
+
+  runner.emit('test', test)
+  if (result.skip || result.todo) {
+    runner.emit('pending', test)
+  } else if (result.ok) {
+    runner.emit('pass', test)
+  } else {
+    var error = getError(result)
+    runner.emit('fail', test, error)
+  }
+  runner.emit('test end', test)
+}
+
+function getError (result) {
+  if (result.diag && result.diag.error)
+    return result.diag.error
+
+  var err = {
+    message: (result.name || '(unnamed error)').replace(/^Error: /, ''),
+    toString: function () {
+      return 'Error: ' + this.message
+    }
+  }
+
+  if (result.diag.stack) {
+    if (Array.isArray(result.diag.stack)) {
+      err.stack = err.toString() + '\n' +
+        result.diag.stack.map(function (s) {
+          return '    at ' + s
+        }).join('\n')
+    } else if (typeof result.diag.stack === 'string') {
+      err.stack = result.diag.stack
+    }
+  }
+
+  var hasFound = Object.prototype.hasOwnProperty.call(result, 'found')
+  var hasWanted = Object.prototype.hasOwnProperty.call(result, 'wanted')
+
+  if (hasFound)
+    err.actual = result.found
+
+  if (hasWanted)
+    err.expected = result.wanted
+
+  if (hasFound && hasWanted)
+    err.showDiff = true
+
+  return err
+}
+
diff --git a/lib/suite.js b/lib/suite.js
new file mode 100644
index 0000000..24b0cb3
--- /dev/null
+++ b/lib/suite.js
@@ -0,0 +1,21 @@
+// minimal mock of mocha's Suite class for formatters
+
+module.exports = Suite
+
+function Suite (parent) {
+  if (!parent.parent || !parent.parent.emittedSuite)
+    this.root = true
+  else
+    this.root = false
+
+  this.title = parent.name
+  this.suites = []
+  this.tests = []
+}
+
+Suite.prototype.fullTitle = function () {
+  if (!this.parent)
+    return this.title || ''
+  else
+    return this.parent.fullTitle() + ' ' + (this.title || '').trim()
+}
diff --git a/lib/test.js b/lib/test.js
new file mode 100644
index 0000000..4511fdb
--- /dev/null
+++ b/lib/test.js
@@ -0,0 +1,28 @@
+// minimal mock of the mocha Test class for formatters
+
+module.exports = Test
+
+function Test (result, parent) {
+  this.result = result
+  this._slow = 75
+  this.duration = result.time
+  this.title = result.name
+  Object.defineProperty(this, 'parent', {
+    value: parent,
+    writable: true,
+    configurable: true,
+    enumerable: false
+  })
+}
+
+Test.prototype.fullTitle = function () {
+  return (this.parent.fullTitle() + ' ' + (this.title || '')).trim()
+}
+
+Test.prototype.slow = function (ms){
+  return 75
+}
+
+Test.prototype.fn = {
+  toString: 'function () {}'
+}
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..ac47027
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,698 @@
+/**
+ * Module dependencies.
+ */
+
+var fs = require('fs')
+  , path = require('path')
+  , basename = path.basename
+  , exists = fs.existsSync || path.existsSync
+  , glob = require('glob')
+  , join = path.join
+  , debug = require('debug')('mocha:watch');
+
+/**
+ * Ignored directories.
+ */
+
+var ignore = ['node_modules', '.git'];
+
+/**
+ * Escape special characters in the given string of html.
+ *
+ * @param  {String} html
+ * @return {String}
+ * @api private
+ */
+
+exports.escape = function(html){
+  return String(html)
+    .replace(/&/g, '&')
+    .replace(/"/g, '"')
+    .replace(/</g, '<')
+    .replace(/>/g, '>');
+};
+
+/**
+ * Array#forEach (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} scope
+ * @api private
+ */
+
+exports.forEach = function(arr, fn, scope){
+  for (var i = 0, l = arr.length; i < l; i++)
+    fn.call(scope, arr[i], i);
+};
+
+/**
+ * Array#map (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} scope
+ * @api private
+ */
+
+exports.map = function(arr, fn, scope){
+  var result = [];
+  for (var i = 0, l = arr.length; i < l; i++)
+    result.push(fn.call(scope, arr[i], i, arr));
+  return result;
+};
+
+/**
+ * Array#indexOf (<=IE8)
+ *
+ * @parma {Array} arr
+ * @param {Object} obj to find index of
+ * @param {Number} start
+ * @api private
+ */
+
+exports.indexOf = function(arr, obj, start){
+  for (var i = start || 0, l = arr.length; i < l; i++) {
+    if (arr[i] === obj)
+      return i;
+  }
+  return -1;
+};
+
+/**
+ * Array#reduce (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} initial value
+ * @api private
+ */
+
+exports.reduce = function(arr, fn, val){
+  var rval = val;
+
+  for (var i = 0, l = arr.length; i < l; i++) {
+    rval = fn(rval, arr[i], i, arr);
+  }
+
+  return rval;
+};
+
+/**
+ * Array#filter (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @api private
+ */
+
+exports.filter = function(arr, fn){
+  var ret = [];
+
+  for (var i = 0, l = arr.length; i < l; i++) {
+    var val = arr[i];
+    if (fn(val, i, arr)) ret.push(val);
+  }
+
+  return ret;
+};
+
+/**
+ * Object.keys (<=IE8)
+ *
+ * @param {Object} obj
+ * @return {Array} keys
+ * @api private
+ */
+
+exports.keys = Object.keys || function(obj) {
+  var keys = []
+    , has = Object.prototype.hasOwnProperty; // for `window` on <=IE8
+
+  for (var key in obj) {
+    if (has.call(obj, key)) {
+      keys.push(key);
+    }
+  }
+
+  return keys;
+};
+
+/**
+ * Watch the given `files` for changes
+ * and invoke `fn(file)` on modification.
+ *
+ * @param {Array} files
+ * @param {Function} fn
+ * @api private
+ */
+
+exports.watch = function(files, fn){
+  var options = { interval: 100 };
+  files.forEach(function(file){
+    debug('file %s', file);
+    fs.watchFile(file, options, function(curr, prev){
+      if (prev.mtime < curr.mtime) fn(file);
+    });
+  });
+};
+
+/**
+ * Array.isArray (<=IE8)
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ * @api private
+ */
+var isArray = Array.isArray || function (obj) {
+  return '[object Array]' == {}.toString.call(obj);
+};
+
+/**
+ * @description
+ * Buffer.prototype.toJSON polyfill
+ * @type {Function}
+ */
+if(typeof Buffer !== 'undefined' && Buffer.prototype) {
+  Buffer.prototype.toJSON = Buffer.prototype.toJSON || function () {
+    return Array.prototype.slice.call(this, 0);
+  };
+}
+
+/**
+ * Ignored files.
+ */
+
+function ignored(path){
+  return !~ignore.indexOf(path);
+}
+
+/**
+ * Lookup files in the given `dir`.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+exports.files = function(dir, ext, ret){
+  ret = ret || [];
+  ext = ext || ['js'];
+
+  var re = new RegExp('\\.(' + ext.join('|') + ')$');
+
+  fs.readdirSync(dir)
+    .filter(ignored)
+    .forEach(function(path){
+      path = join(dir, path);
+      if (fs.statSync(path).isDirectory()) {
+        exports.files(path, ext, ret);
+      } else if (path.match(re)) {
+        ret.push(path);
+      }
+    });
+
+  return ret;
+};
+
+/**
+ * Compute a slug from the given `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+exports.slug = function(str){
+  return str
+    .toLowerCase()
+    .replace(/ +/g, '-')
+    .replace(/[^-\w]/g, '');
+};
+
+/**
+ * Strip the function definition from `str`,
+ * and re-indent for pre whitespace.
+ */
+
+exports.clean = function(str) {
+  str = str
+    .replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, '')
+    .replace(/^function *\(.*\) *{|\(.*\) *=> *{?/, '')
+    .replace(/\s+\}$/, '');
+
+  var spaces = str.match(/^\n?( *)/)[1].length
+    , tabs = str.match(/^\n?(\t*)/)[1].length
+    , re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm');
+
+  str = str.replace(re, '');
+
+  return exports.trim(str);
+};
+
+/**
+ * Trim the given `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+exports.trim = function(str){
+  return str.replace(/^\s+|\s+$/g, '');
+};
+
+/**
+ * Parse the given `qs`.
+ *
+ * @param {String} qs
+ * @return {Object}
+ * @api private
+ */
+
+exports.parseQuery = function(qs){
+  return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){
+    var i = pair.indexOf('=')
+      , key = pair.slice(0, i)
+      , val = pair.slice(++i);
+
+    obj[key] = decodeURIComponent(val);
+    return obj;
+  }, {});
+};
+
+/**
+ * Highlight the given string of `js`.
+ *
+ * @param {String} js
+ * @return {String}
+ * @api private
+ */
+
+function highlight(js) {
+  return js
+    .replace(/</g, '<')
+    .replace(/>/g, '>')
+    .replace(/\/\/(.*)/gm, '<span class="comment">//$1</span>')
+    .replace(/('.*?')/gm, '<span class="string">$1</span>')
+    .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>')
+    .replace(/(\d+)/gm, '<span class="number">$1</span>')
+    .replace(/\bnew[ \t]+(\w+)/gm, '<span class="keyword">new</span> <span class="init">$1</span>')
+    .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '<span class="keyword">$1</span>')
+}
+
+/**
+ * Highlight the contents of tag `name`.
+ *
+ * @param {String} name
+ * @api private
+ */
+
+exports.highlightTags = function(name) {
+  var code = document.getElementById('mocha').getElementsByTagName(name);
+  for (var i = 0, len = code.length; i < len; ++i) {
+    code[i].innerHTML = highlight(code[i].innerHTML);
+  }
+};
+
+/**
+ * If a value could have properties, and has none, this function is called, which returns
+ * a string representation of the empty value.
+ *
+ * Functions w/ no properties return `'[Function]'`
+ * Arrays w/ length === 0 return `'[]'`
+ * Objects w/ no properties return `'{}'`
+ * All else: return result of `value.toString()`
+ *
+ * @param {*} value Value to inspect
+ * @param {string} [type] The type of the value, if known.
+ * @returns {string}
+ */
+var emptyRepresentation = function emptyRepresentation(value, type) {
+  type = type || exports.type(value);
+
+  switch(type) {
+    case 'function':
+      return '[Function]';
+    case 'object':
+      return '{}';
+    case 'array':
+      return '[]';
+    default:
+      return value.toString();
+  }
+};
+
+/**
+ * Takes some variable and asks `{}.toString()` what it thinks it is.
+ * @param {*} value Anything
+ * @example
+ * type({}) // 'object'
+ * type([]) // 'array'
+ * type(1) // 'number'
+ * type(false) // 'boolean'
+ * type(Infinity) // 'number'
+ * type(null) // 'null'
+ * type(new Date()) // 'date'
+ * type(/foo/) // 'regexp'
+ * type('type') // 'string'
+ * type(global) // 'global'
+ * @api private
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
+ * @returns {string}
+ */
+exports.type = function type(value) {
+  if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
+    return 'buffer';
+  }
+  return Object.prototype.toString.call(value)
+    .replace(/^\[.+\s(.+?)\]$/, '$1')
+    .toLowerCase();
+};
+
+/**
+ * @summary Stringify `value`.
+ * @description Different behavior depending on type of value.
+ * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively.
+ * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes.
+ * - If `value` is an *empty* object, function, or array, return result of function
+ *   {@link emptyRepresentation}.
+ * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of
+ *   JSON.stringify().
+ *
+ * @see exports.type
+ * @param {*} value
+ * @return {string}
+ * @api private
+ */
+
+exports.stringify = function(value) {
+  var type = exports.type(value);
+
+  if (!~exports.indexOf(['object', 'array', 'function'], type)) {
+    if(type != 'buffer') {
+      return jsonStringify(value);
+    }
+    var json = value.toJSON();
+    // Based on the toJSON result
+    return jsonStringify(json.data && json.type ? json.data : json, 2)
+      .replace(/,(\n|$)/g, '$1');
+  }
+
+  for (var prop in value) {
+    if (Object.prototype.hasOwnProperty.call(value, prop)) {
+      return jsonStringify(exports.canonicalize(value), 2).replace(/,(\n|$)/g, '$1');
+    }
+  }
+
+  return emptyRepresentation(value, type);
+};
+
+/**
+ * @description
+ * like JSON.stringify but more sense.
+ * @param {Object}  object
+ * @param {Number=} spaces
+ * @param {number=} depth
+ * @returns {*}
+ * @private
+ */
+function jsonStringify(object, spaces, depth) {
+  if(typeof spaces == 'undefined') return _stringify(object);  // primitive types
+
+  depth = depth || 1;
+  var space = spaces * depth
+    , str = isArray(object) ? '[' : '{'
+    , end = isArray(object) ? ']' : '}'
+    , length = object.length || exports.keys(object).length
+    , repeat = function(s, n) { return new Array(n).join(s); }; // `.repeat()` polyfill
+
+  function _stringify(val) {
+    switch (exports.type(val)) {
+      case 'null':
+      case 'undefined':
+        val = '[' + val + ']';
+        break;
+      case 'array':
+      case 'object':
+        val = jsonStringify(val, spaces, depth + 1);
+        break;
+      case 'boolean':
+      case 'regexp':
+      case 'number':
+        val = val === 0 && (1/val) === -Infinity // `-0`
+          ? '-0'
+          : val.toString();
+        break;
+      case 'date':
+        val = '[Date: ' + val.toISOString() + ']';
+        break;
+      case 'buffer':
+        var json = val.toJSON();
+        // Based on the toJSON result
+        json = json.data && json.type ? json.data : json;
+        val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']';
+        break;
+      default:
+        val = (val == '[Function]' || val == '[Circular]')
+          ? val
+          : '"' + val + '"'; //string
+    }
+    return val;
+  }
+
+  for(var i in object) {
+    if(!object.hasOwnProperty(i)) continue;        // not my business
+    --length;
+    str += '\n ' + repeat(' ', space)
+      + (isArray(object) ? '' : '"' + i + '": ') // key
+      +  _stringify(object[i])                   // value
+      + (length ? ',' : '');                     // comma
+  }
+
+  return str + (str.length != 1                    // [], {}
+    ? '\n' + repeat(' ', --space) + end
+    : end);
+}
+
+/**
+ * Return if obj is a Buffer
+ * @param {Object} arg
+ * @return {Boolean}
+ * @api private
+ */
+exports.isBuffer = function (arg) {
+  return typeof Buffer !== 'undefined' && Buffer.isBuffer(arg);
+};
+
+/**
+ * @summary Return a new Thing that has the keys in sorted order.  Recursive.
+ * @description If the Thing...
+ * - has already been seen, return string `'[Circular]'`
+ * - is `undefined`, return string `'[undefined]'`
+ * - is `null`, return value `null`
+ * - is some other primitive, return the value
+ * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method
+ * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again.
+ * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()`
+ *
+ * @param {*} value Thing to inspect.  May or may not have properties.
+ * @param {Array} [stack=[]] Stack of seen values
+ * @return {(Object|Array|Function|string|undefined)}
+ * @see {@link exports.stringify}
+ * @api private
+ */
+
+exports.canonicalize = function(value, stack) {
+  var canonicalizedObj,
+    type = exports.type(value),
+    prop,
+    withStack = function withStack(value, fn) {
+      stack.push(value);
+      fn();
+      stack.pop();
+    };
+
+  stack = stack || [];
+
+  if (exports.indexOf(stack, value) !== -1) {
+    return '[Circular]';
+  }
+
+  switch(type) {
+    case 'undefined':
+    case 'buffer':
+    case 'null':
+      canonicalizedObj = value;
+      break;
+    case 'array':
+      withStack(value, function () {
+        canonicalizedObj = exports.map(value, function (item) {
+          return exports.canonicalize(item, stack);
+        });
+      });
+      break;
+    case 'function':
+      for (prop in value) {
+        canonicalizedObj = {};
+        break;
+      }
+      if (!canonicalizedObj) {
+        canonicalizedObj = emptyRepresentation(value, type);
+        break;
+      }
+    /* falls through */
+    case 'object':
+      canonicalizedObj = canonicalizedObj || {};
+      withStack(value, function () {
+        exports.forEach(exports.keys(value).sort(), function (key) {
+          canonicalizedObj[key] = exports.canonicalize(value[key], stack);
+        });
+      });
+      break;
+    case 'date':
+    case 'number':
+    case 'regexp':
+    case 'boolean':
+      canonicalizedObj = value;
+      break;
+    default:
+      canonicalizedObj = value.toString();
+  }
+
+  return canonicalizedObj;
+};
+
+/**
+ * Lookup file names at the given `path`.
+ */
+exports.lookupFiles = function lookupFiles(path, extensions, recursive) {
+  var files = [];
+  var re = new RegExp('\\.(' + extensions.join('|') + ')$');
+
+  if (!exists(path)) {
+    if (exists(path + '.js')) {
+      path += '.js';
+    } else {
+      files = glob.sync(path);
+      if (!files.length) throw new Error("cannot resolve path (or pattern) '" + path + "'");
+      return files;
+    }
+  }
+
+  try {
+    var stat = fs.statSync(path);
+    if (stat.isFile()) return path;
+  }
+  catch (ignored) {
+    return;
+  }
+
+  fs.readdirSync(path).forEach(function(file) {
+    file = join(path, file);
+    try {
+      var stat = fs.statSync(file);
+      if (stat.isDirectory()) {
+        if (recursive) {
+          files = files.concat(lookupFiles(file, extensions, recursive));
+        }
+        return;
+      }
+    }
+    catch (ignored) {
+      return;
+    }
+    if (!stat.isFile() || !re.test(file) || basename(file)[0] === '.') return;
+    files.push(file);
+  });
+
+  return files;
+};
+
+/**
+ * Generate an undefined error with a message warning the user.
+ *
+ * @return {Error}
+ */
+
+exports.undefinedError = function() {
+  return new Error('Caught undefined error, did you throw without specifying what?');
+};
+
+/**
+ * Generate an undefined error if `err` is not defined.
+ *
+ * @param {Error} err
+ * @return {Error}
+ */
+
+exports.getError = function(err) {
+  return err || exports.undefinedError();
+};
+
+
+/**
+ * @summary
+ * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`)
+ * @description
+ * When invoking this function you get a filter function that get the Error.stack as an input,
+ * and return a prettify output.
+ * (i.e: strip Mocha, node_modules, bower and componentJS from stack trace).
+ * @returns {Function}
+ */
+
+exports.stackTraceFilter = function() {
+  var slash = '/'
+    , is = typeof document === 'undefined'
+      ? { node: true }
+      : { browser: true }
+    , cwd = is.node
+      ? process.cwd() + slash
+      : location.href.replace(/\/[^\/]*$/, '/');
+
+  function isNodeModule (line) {
+    return (~line.indexOf('node_modules'));
+  }
+
+  function isMochaInternal (line) {
+    return (~line.indexOf('node_modules' + slash + 'tap-mocha-reporter'))  ||
+      (~line.indexOf('components' + slash + 'mochajs'))       ||
+      (~line.indexOf('components' + slash + 'mocha'));
+  }
+
+  // node_modules, bower, componentJS
+  function isBrowserModule(line) {
+    return (~line.indexOf('node_modules')) ||
+      (~line.indexOf('components'));
+  }
+
+  function isNodeInternal (line) {
+    return (~line.indexOf('(timers.js:')) ||
+      (~line.indexOf('(domain.js:'))      ||
+      (~line.indexOf('(events.js:'))      ||
+      (~line.indexOf('(node.js:'))        ||
+      (~line.indexOf('(module.js:'))      ||
+      (~line.indexOf('at node.js:'))      ||
+      (~line.indexOf('GeneratorFunctionPrototype.next (native)')) ||
+      false
+  }
+
+  return function(stack) {
+    stack = stack.split('\n');
+
+    stack = stack.reduce(function (list, line) {
+      if (is.node && (isNodeModule(line) ||
+        isMochaInternal(line) ||
+        isNodeInternal(line)))
+        return list;
+
+      if (is.browser && (isBrowserModule(line)))
+        return list;
+
+      // Clean up cwd(absolute)
+      list.push(line.replace(cwd, ''));
+      return list;
+    }, []);
+
+    return stack.join('\n');
+  }
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..83a2cbd
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+  "name": "tap-spec-reporter",
+  "version": "0.0.0",
+  "description": "Format TAP with indented sections to spec-like colored checks and exes.",
+  "main": "index.js",
+  "scripts": {
+    "test": "tap test/*.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/isaacs/tap-spec-reporter"
+  },
+  "author": "Isaac Z. Schlueter <i at izs.me> (http://blog.izs.me/)",
+  "license": "ISC",
+  "bugs": {
+    "url": "https://github.com/isaacs/tap-spec-reporter/issues"
+  },
+  "homepage": "https://github.com/isaacs/tap-spec-reporter",
+  "dependencies": {
+    "supports-color": "^1.3.1",
+    "tap-parser": "^1.0.4"
+  },
+  "bin": "index.js"
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-tap-mocha-reporter.git



More information about the Pkg-javascript-commits mailing list