TBR playground/client: add JSON stream test coverage

* Add tests for error and edge cases for the JSONStream
* api.run() returns stream directly instead of having the extra callback.
* Add initial message to let the user know that the run has started.

Change-Id: I9056da42389c4b4e3dd62e00b9e0b2d69175800b
Related: https://github.com/vanadium/issues/issues/38
diff --git a/client/browser/api/index.js b/client/browser/api/index.js
index c13a196..5c082f1 100644
--- a/client/browser/api/index.js
+++ b/client/browser/api/index.js
@@ -12,8 +12,7 @@
 var extend = require('xtend');
 var prr = require('prr');
 var config = require('../config');
-var JSONStream = require('./json-stream');
-var split = require('split');
+var jsonStream = require('./json-stream');
 var defaults = {
   // Timeout for HTTP requests, 5 secs in milliseconds.
   timeout: 5 * 60 * 1000,
@@ -204,7 +203,7 @@
 // immediately.
 // TODO(jasoncampbell): stop pending xhr
 // SEE: https://github.com/vanadium/issues/issues/39
-API.prototype.run = function(data, callback) {
+API.prototype.run = function(data) {
   var api = this;
   var uuid = data.uuid;
   var uri = api.url({
@@ -212,10 +211,12 @@
         debug: true
       });
 
+  // NOTE: The UI prevents the channel from being fired while the run is in
+  // progress so this should never happen.
   if (api.isPending(uuid)) {
     var message = format('%s is already running');
     var err = new Error(message);
-    return callback(err);
+    throw err;
   }
 
   api.pending(uuid);
@@ -231,23 +232,20 @@
   // TODO(jasoncampbell): Consolidate http libraries.
   // TODO(jasoncampbell): Verify XHR timeout logic and handle appropriately.
   var req = hyperquest.post(uri, options);
+  var stream = jsonStream();
 
-  req.once('error', callback);
+  req.on('error', function(err) {
+    stream.emit('error', err);
+  });
 
-  var stream = JSONStream(); // jshint ignore:line
-
-  stream.on('end', function() {
+  req.on('end', function() {
     api.done(uuid);
   });
 
-  req
-  .pipe(split())
-  .pipe(stream);
+  req.pipe(stream);
 
-  callback(null, stream);
-
-  var string = JSON.stringify(data);
-
-  req.write(string);
+  req.write(JSON.stringify(data));
   req.end();
+
+  return stream;
 };
diff --git a/client/browser/api/json-stream.js b/client/browser/api/json-stream.js
index 6436c67..4834fa2 100644
--- a/client/browser/api/json-stream.js
+++ b/client/browser/api/json-stream.js
@@ -2,27 +2,21 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-var through2 = require('through2');
+// Using jxson/split#ignore-trailing until it is merged upstream.
+//
+// SEE: https://github.com/dominictarr/split/pull/15
+var split = require('split');
 
-module.exports = create;
+module.exports = JSONStream;
 
-function create() {
-  return through2.obj(write);
+function JSONStream() {
+  return split(parse, null, { trailing: false });
 }
 
-function write(buffer, enc, callback) {
+function parse(buffer) {
   if (buffer.length === 0) {
-    return callback();
+    return undefined;
+  } else {
+    return JSON.parse(buffer);
   }
-
-  var json;
-  var err;
-
-  try {
-    json = JSON.parse(buffer);
-  } catch (err) {
-    err.data = buffer;
-  }
-
-  callback(err, json);
 }
diff --git a/client/browser/components/bundle/index.js b/client/browser/components/bundle/index.js
index d31995f..30de7cd 100644
--- a/client/browser/components/bundle/index.js
+++ b/client/browser/components/bundle/index.js
@@ -115,27 +115,30 @@
   debug('running');
   state.running.set(true);
 
+  // Temporary message to provide feedback and show that the run is happening.
+  state.results.logs.push(log({
+    File: 'web client',
+    Message: 'Run request initiated.',
+    Stream: 'stdout'
+  }));
+
   var data = {
     uuid: state.uuid(),
     files: toArray(state.files())
   };
 
-  api.run(data, function onrun(err, stream) {
-    if (err) {
-      // TODO(jasoncampbell): handle error appropriately.
-      throw err;
-    }
+  var stream = api.run(data);
 
-    stream.on('error', function onerror(err) {
-      throw err;
-    });
+  stream.on('error', function(err) {
+    // TODO(jasoncampbell): handle errors appropriately.
+    throw err;
+  });
 
-    stream.on('data', function ondata(data) {
-      state.results.logs.push(log(data));
-    });
+  stream.on('data', function ondata(data) {
+    state.results.logs.push(log(data));
+  });
 
-    stream.on('end', function() {
-      state.running.set(false);
-    });
+  stream.on('end', function() {
+    state.running.set(false);
   });
 }
diff --git a/client/browser/components/log/normalize.js b/client/browser/components/log/normalize.js
index 11ccd37..c31521a 100644
--- a/client/browser/components/log/normalize.js
+++ b/client/browser/components/log/normalize.js
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+var now = require('date-now');
+
 module.exports = normalize;
 
 // Takes data from API messages and converts them to appropriate objects
@@ -9,7 +11,7 @@
 function normalize(data) {
   // convert `data.Timestamp` nanosecond value to a float in milliseconds.
   var oneMillion = 1e6;
-  var timestamp = data.Timestamp / oneMillion;
+  var timestamp = data.Timestamp ? (data.Timestamp / oneMillion) : now();
 
   return {
     message: data.Message,
diff --git a/client/browser/exception-logger.js b/client/browser/exception-logger.js
index bbd0139..7bafe88 100644
--- a/client/browser/exception-logger.js
+++ b/client/browser/exception-logger.js
@@ -5,7 +5,9 @@
 var window = require('global/window');
 var UA = window.navigator ? window.navigator.userAgent : '';
 
-module.exports = init;
+module.exports = {
+  init: init
+};
 
 function init() {
   window.addEventListener('error', onerror);
diff --git a/client/package.json b/client/package.json
index fdea47f..8a9c96f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -9,6 +9,7 @@
   "main": "browser/index.js",
   "dependencies": {
     "brace": "^0.4.0",
+    "date-now": "^1.0.1",
     "debug": "^2.1.3",
     "domready": "^1.0.7",
     "format": "^0.2.1",
@@ -20,7 +21,7 @@
     "routes": "^2.0.0",
     "run-parallel": "^1.1.0",
     "single-page": "^1.0.0",
-    "split": "^0.3.3",
+    "split": "jxson/split#ignore-trailing",
     "superagent": "^1.1.0",
     "through2": "^0.6.3",
     "xtend": "^4.0.0"
diff --git a/client/test/index.js b/client/test/index.js
index 6cf839a..5dd9c9c 100644
--- a/client/test/index.js
+++ b/client/test/index.js
@@ -5,3 +5,5 @@
 require('./test-api-url');
 require('./test-config');
 require('./test-component-log');
+require('./test-component-results');
+require('./test-api-json-stream');
diff --git a/client/test/test-api-json-stream.js b/client/test/test-api-json-stream.js
new file mode 100644
index 0000000..c0e8106
--- /dev/null
+++ b/client/test/test-api-json-stream.js
@@ -0,0 +1,138 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var test = require('tape');
+var jsonStream = require('../browser/api/json-stream');
+var Buffer = require('buffer').Buffer;
+
+test('jsonStream() - single entity per write', function(t) {
+  t.plan(4);
+
+  var stream = jsonStream();
+
+  stream
+  .on('data', function(data) {
+    t.same(data, { foo: 'bar' });
+  });
+
+  iterate(4, function() {
+    stream.write(new Buffer('{ "foo": "bar" }\n'));
+  });
+
+  stream.end();
+});
+
+test('jsonStream() - empty strings', function(t) {
+  t.plan(1);
+
+  var stream = jsonStream();
+
+  stream
+  .on('data', function(data) {
+    t.same(data, { foo: 'bar' });
+  });
+
+  stream.write(new Buffer('{ "foo": "bar" }\n'));
+  stream.write(new Buffer(''));
+
+  stream.end();
+});
+
+test('jsonStream() - single entry, multiple writes', function(t) {
+  t.plan(1);
+
+  var stream = jsonStream();
+
+  stream
+  .on('data', function(data) {
+    t.same(data, { foo: 'bar', baz: 'qux' });
+  });
+
+  stream.write(new Buffer('{ "foo":'));
+  stream.write(new Buffer('"ba'));
+  stream.write(new Buffer('r", "ba'));
+  stream.write(new Buffer('z":'));
+  stream.write(new Buffer('"qux" }\n'));
+  stream.end();
+});
+
+test('jsonStream() - chunk with several entries, then pieces', function(t) {
+  t.plan(11);
+
+  var stream = jsonStream();
+  var chunk = '';
+
+  stream
+  .on('data', function(data) {
+    t.same(data, { a: 'b' });
+  });
+
+  iterate(10, function() {
+    chunk += '{ "a": "b" }\n';
+  });
+
+  chunk += '{ "a":';
+
+  stream.write(new Buffer(chunk));
+  stream.write(new Buffer(' "b" }\n'));
+  stream.end();
+});
+
+test('jsonStream() - end with a partial entry', function(t) {
+  t.plan(2);
+
+  var stream = jsonStream();
+
+  stream
+  .on('end', t.end)
+  .on('error', function(err) {
+    t.fail('should not error');
+  })
+  .on('data', function(data) {
+    t.same(data, { a: 'b' });
+  });
+
+  stream.write('{ "a": "b" }\n');
+  stream.write('{ "a": "b" }\n');
+  stream.write('{ "a": ');
+  stream.end();
+});
+
+test('jsonStream() - blank line entries', function(t) {
+  t.plan(1);
+
+  var stream = jsonStream();
+
+  stream
+  .on('end', t.end)
+  .on('error', function(err) {
+    t.fail('should not error');
+  })
+  .on('data', function(data) {
+    t.same(data, { a: 'b' });
+  });
+
+  stream.write('{ "a": "b" }\n');
+  stream.write('');
+  stream.end();
+});
+
+test('jsonStream() - bad json entry', function(t) {
+  var stream = jsonStream();
+  var chunk = '{ bad: "json"';
+
+  stream.on('error', function(err) {
+    t.ok(err instanceof Error, 'should error');
+    t.ok(err instanceof SyntaxError, 'should be a SyntaxError');
+    t.end();
+  });
+
+  stream.write(new Buffer(chunk + '\n'));
+});
+
+function iterate(times, iterator) {
+  for (var i = 0; i < times; i++) {
+    iterator(i);
+  }
+}