Repository: amzn/ion-js
Pull request #634 (2020-09-29 17:55:59)

Test driver Integration
This PR is to integrate ion-test-driver code into ion-js.

For reference, here is the review links:

1. test-driver cli: https://github.com/amzn/ion-js/pull/624
2. test-driver executable and integration: https://github.com/desaikd/ion-js/pull/3 



Patch:
diff --git a/.gitignore b/.gitignore
index cf9e48c..9791455 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,8 @@
 .tscache/
 dist/
+test-driver/dist
 node_modules/
-src/.baseDir.ts
+**/.baseDir.ts
 lcov.info
 package-lock.json
 browser/scripts
@@ -41,7 +42,6 @@ tramp
 
 # cask packages
 .cask/
-dist/
 
 # Flycheck
 flycheck_*.el
diff --git a/Gruntfile.js b/Gruntfile.js
index 75dfa4f..bd9a8d0 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -16,11 +16,14 @@
 module.exports = function(grunt) {
   grunt.initConfig({
     pkg: grunt.file.readJSON('package.json'),
-    clean: ['dist/',
-            'docs/',
-            'coverage-final.json',
-            'browser/scripts/ion/'
-           ],
+    clean: {
+      'ion-js': ['dist/',
+                 'docs/',
+                 'coverage-final.json',
+                 'browser/scripts/ion/'
+                ],
+      'test-driver': ['test-driver/dist/']
+    },
     jshint: {
       files: []
     },
@@ -41,6 +44,9 @@ module.exports = function(grunt) {
       },
       'commonjs-es6': {
         tsconfig: 'tsconfig.commonjs.json'
+      },
+      'test-driver' : {
+        tsconfig: './test-driver/tsconfig.json'
       }
     },
     tslint: {
@@ -165,8 +171,10 @@ module.exports = function(grunt) {
   grunt.registerTask('build:es6', ['ts:es6-es6']);
   grunt.registerTask('build:cjs', ['ts:commonjs-es6']);
   grunt.registerTask('build:amd', ['ts:amd-es6']);
+  grunt.registerTask('build-tsc:test-driver', ['ts:test-driver']);
+  grunt.registerTask('build:test-driver', ['clean:test-driver', 'build:cjs', 'build-tsc:test-driver']);
   grunt.registerTask('build', [
-      'clean', 'build:es6', 'build:amd', 'build:cjs', 'trans:browser', 'copy:all'
+      'clean:ion-js', 'build:es6', 'build:amd', 'build:cjs', 'trans:browser', 'copy:all'
   ]);
 
   // Temporary targets that will eventually replace 'test' and 'test:coverage'
@@ -184,13 +192,9 @@ module.exports = function(grunt) {
 
   grunt.registerTask('doc', ['typedoc']);
 
-  grunt.registerTask('cleanup', 'Removes extraneous files from distributions', function() {
-    grunt.file.expand('dist/**/IonEvent*').forEach((file) => grunt.file.delete(file));
-  });
-
   // release target used by Travis 
-  grunt.registerTask('release', ['test:coverage', 'build', 'cleanup', 'typedoc', 'nojekyll']);
+  grunt.registerTask('release', ['test:coverage', 'build', 'typedoc', 'nojekyll']);
 
   // default for development
-  grunt.registerTask('default', ['test', 'cleanup']);
+  grunt.registerTask('default', ['test']);
 };
diff --git a/package.json b/package.json
index 467b6ec..02aadea 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,8 @@
     "prepare": "grunt release",
     "test": "nyc mocha",
     "release": "grunt release",
+    "test-driver": "cd test-driver && npm install",
+    "build-test-driver": "cd test-driver && npm run build",
     "prettier-check": "prettier --check 'src/**/*.ts'"
   },
   "repository": {
@@ -80,7 +82,8 @@
     "ts-node": "^8.10.1",
     "tslint": "^5.20.1",
     "typedoc": "^0.16.10",
-    "typescript": "^3.9.3"
+    "typescript": "^3.9.3",
+    "yargs": "^15.4.1"
   },
   "peerDependencies": {
     "jsbi": "^3.1.1"
diff --git a/src/ComparisonResult.ts b/src/ComparisonResult.ts
new file mode 100644
index 0000000..254982e
--- /dev/null
+++ b/src/ComparisonResult.ts
@@ -0,0 +1,45 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+/** Comparison result types for the comparison report
+ *  EQUALS: Indicates the input streams are equal
+ *  NOT_EQUAL: Indicates the input streams are not equal
+ *  ERROR: For all the cases where an error occurs while reading or writing input streams
+ */
+export enum ComparisonResultType {
+  EQUAL = "EQUAL",
+  NOT_EQUAL = "NOT_EQUAL",
+  ERROR = "ERROR",
+}
+
+/** comparison result with event index and message **/
+export class ComparisonResult {
+  message: string;
+  result: ComparisonResultType;
+  actualIndex: number;
+  expectedIndex: number;
+
+  constructor(
+    result: ComparisonResultType = ComparisonResultType.EQUAL,
+    message: string = "",
+    actualIndex: number = 0,
+    expectedIndex: number = 0
+  ) {
+    this.result = result;
+    this.message = message;
+    this.actualIndex = actualIndex;
+    this.expectedIndex = expectedIndex;
+  }
+}
diff --git a/src/Ion.ts b/src/Ion.ts
index fb59aa8..b0c4ae0 100644
--- a/src/Ion.ts
+++ b/src/Ion.ts
@@ -139,3 +139,9 @@ export { dom };
 
 // Re-export dom convenience methods for easy access via 'ion'
 export { load, loadAll } from "./dom";
+
+// Events exports and Comparison Result export
+export { IonEvent, IonEventType, IonEventFactory } from "./events/IonEvent";
+export { IonEventStream } from "./events/IonEventStream";
+export { EventStreamError } from "./events/EventStreamError";
+export { ComparisonResult, ComparisonResultType } from "./ComparisonResult";
diff --git a/src/IonEventStream.ts b/src/IonEventStream.ts
deleted file mode 100644
index dd256d6..0000000
--- a/src/IonEventStream.ts
+++ /dev/null
@@ -1,489 +0,0 @@
-/*!
- * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-import { Reader } from "./IonReader";
-import { IonEvent, IonEventFactory, IonEventType } from "./IonEvent";
-import { Writer } from "./IonWriter";
-import { IonType } from "./IonType";
-import { IonTypes } from "./IonTypes";
-import { makeReader } from "./Ion";
-
-export class IonEventStream {
-  private eventStream: IonEvent[];
-  private reader: Reader;
-  private eventFactory: IonEventFactory;
-
-  constructor(reader: Reader) {
-    this.eventStream = [];
-    this.reader = reader;
-    this.eventFactory = new IonEventFactory();
-    this.generateStream();
-  }
-
-  writeEventStream(writer: Writer) {
-    writer.writeSymbol("ion_event_stream");
-    for (let i: number = 0; i < this.eventStream.length; i++) {
-      this.eventStream[i].write(writer);
-    }
-  }
-
-  writeIon(writer: Writer) {
-    let tempEvent: IonEvent;
-    for (let indice: number = 0; indice < this.eventStream.length; indice++) {
-      tempEvent = this.eventStream[indice];
-      if (tempEvent.fieldName !== null) {
-        writer.writeFieldName(tempEvent.fieldName);
-      }
-      writer.setAnnotations(tempEvent.annotations);
-      switch (tempEvent.eventType) {
-        case IonEventType.SCALAR:
-          if (tempEvent.ionValue == null) {
-            writer.writeNull(tempEvent.ionType!);
-            return;
-          }
-          switch (tempEvent.ionType) {
-            case IonTypes.BOOL:
-              writer.writeBoolean(tempEvent.ionValue);
-              break;
-            case IonTypes.STRING:
-              writer.writeString(tempEvent.ionValue);
-              break;
-            case IonTypes.SYMBOL:
-              writer.writeSymbol(tempEvent.ionValue);
-              break;
-            case IonTypes.INT:
-              writer.writeInt(tempEvent.ionValue);
-              break;
-            case IonTypes.DECIMAL:
-              writer.writeDecimal(tempEvent.ionValue);
-              break;
-            case IonTypes.FLOAT:
-              writer.writeFloat64(tempEvent.ionValue);
-              break;
-            case IonTypes.NULL:
-              writer.writeNull(tempEvent.ionType);
-              break;
-            case IonTypes.TIMESTAMP:
-              writer.writeTimestamp(tempEvent.ionValue);
-              break;
-            case IonTypes.CLOB:
-              writer.writeClob(tempEvent.ionValue);
-              break;
-            case IonTypes.BLOB:
-              writer.writeBlob(tempEvent.ionValue);
-              break;
-            default:
-              throw new Error("unexpected type: " + tempEvent.ionType!.name);
-          }
-          break;
-        case IonEventType.CONTAINER_START:
-          writer.stepIn(tempEvent.ionType!);
-          break;
-        case IonEventType.CONTAINER_END:
-          writer.stepOut();
-          break;
-        case IonEventType.STREAM_END:
-          writer.close();
-          break;
-        case IonEventType.SYMBOL_TABLE:
-          throw new Error("Symboltables unsupported.");
-        default:
-          throw new Error("Unexpected event type: " + tempEvent.eventType);
-      }
-    }
-  }
-
-  getEvents(): IonEvent[] {
-    return this.eventStream;
-  }
-
-  equals(expected: IonEventStream) {
-    let actualIndex: number = 0;
-    let expectedIndex: number = 0;
-    while (
-      actualIndex < this.eventStream.length &&
-      expectedIndex < expected.eventStream.length
-    ) {
-      let actualEvent = this.eventStream[actualIndex];
-      let expectedEvent = expected.eventStream[expectedIndex];
-      if (actualEvent.eventType === IonEventType.SYMBOL_TABLE) actualIndex++;
-      if (expectedEvent.eventType === IonEventType.SYMBOL_TABLE)
-        expectedIndex++;
-      if (
-        actualEvent.eventType === IonEventType.SYMBOL_TABLE ||
-        expectedEvent.eventType === IonEventType.SYMBOL_TABLE
-      )
-        continue;
-      switch (actualEvent.eventType) {
-        case IonEventType.SCALAR: {
-          if (!actualEvent.equals(expectedEvent)) return false;
-          break;
-        }
-        case IonEventType.CONTAINER_START: {
-          if (actualEvent.equals(expectedEvent)) {
-            actualIndex = actualIndex + actualEvent.ionValue.length;
-            expectedIndex = expectedIndex + expectedEvent.ionValue.length;
-          } else {
-            return false;
-          }
-          break;
-        }
-        case IonEventType.CONTAINER_END:
-        case IonEventType.STREAM_END: {
-          //no op
-          break;
-        }
-        default: {
-          throw new Error("Unexpected event type: " + actualEvent.eventType);
-        }
-      }
-      actualIndex++;
-      expectedIndex++;
-    }
-    return true;
-  }
-
-  private generateStream(): void {
-    let tid: IonType | null = this.reader.next();
-    if (
-      tid === IonTypes.SYMBOL &&
-      this.reader.stringValue() === "ion_event_stream"
-    ) {
-      this.marshalStream();
-      return;
-    }
-    let currentContainer: IonEvent[] = [];
-    let currentContainerIndex: number[] = [];
-    while (true) {
-      if (this.reader.isNull()) {
-        this.eventStream.push(
-          this.eventFactory.makeEvent(
-            IonEventType.SCALAR,
-            tid!,
-            this.reader.fieldName(),
-            this.reader.depth(),
-            this.reader.annotations(),
-            true,
-            this.reader.value()
-          )
-        );
-      } else {
-        switch (tid) {
-          case IonTypes.LIST:
-          case IonTypes.SEXP:
-          case IonTypes.STRUCT: {
-            let containerEvent = this.eventFactory.makeEvent(
-              IonEventType.CONTAINER_START,
-              tid,
-              this.reader.fieldName(),
-              this.reader.depth(),
-              this.reader.annotations(),
-              false,
-              null
-            );
-            this.eventStream.push(containerEvent);
-            currentContainer.push(containerEvent);
-            currentContainerIndex.push(this.eventStream.length);
-            this.reader.stepIn();
-            break;
-          }
-          case null: {
-            if (this.reader.depth() === 0) {
-              this.eventStream.push(
-                this.eventFactory.makeEvent(
-                  IonEventType.STREAM_END,
-                  IonTypes.NULL,
-                  null,
-                  this.reader.depth(),
-                  [],
-                  false,
-                  undefined
-                )
-              );
-              return;
-            } else {
-              this.reader.stepOut();
-              this.endContainer(
-                currentContainer.pop()!,
-                currentContainerIndex.pop()!
-              );
-            }
-            break;
-          }
-          default: {
-            this.eventStream.push(
-              this.eventFactory.makeEvent(
-                IonEventType.SCALAR,
-                tid,
-                this.reader.fieldName(),
-                this.reader.depth(),
-                this.reader.annotations(),
-                false,
-                this.reader.value()
-              )
-            );
-            break;
-          }
-        }
-      }
-      tid = this.reader.next();
-    }
-  }
-
-  private endContainer(thisContainer: IonEvent, thisContainerIndex: number) {
-    this.eventStream.push(
-      this.eventFactory.makeEvent(
-        IonEventType.CONTAINER_END,
-        thisContainer.ionType!,
-        null,
-        thisContainer.depth,
-        [],
-        false,
-        null
-      )
-    );
-    thisContainer.ionValue = this.eventStream.slice(
-      thisContainerIndex,
-      this.eventStream.length
-    );
-  }
-
-  //(event_type: EventType, ion_type: IonType, field_name: SymbolToken, annotations: list<SymbolToken>, value_text: string, value_binary: list<byte>, imports: list<ImportDescriptor>, depth: int)
-  private marshalStream(): void {
-    this.eventStream = [];
-    let currentContainer: IonEvent[] = [];
-    let currentContainerIndex: number[] = [];
-    for (
-      let tid: IonType | null = this.reader.next();
-      tid === IonTypes.STRUCT;
-      tid = this.reader.next()
-    ) {
-      this.reader.stepIn();
-      let tempEvent: IonEvent = this.marshalEvent();
-      if (tempEvent.eventType === IonEventType.CONTAINER_START) {
-        currentContainer.push(tempEvent);
-        this.eventStream.push(tempEvent);
-        currentContainerIndex.push(this.eventStream.length);
-      } else if (tempEvent.eventType === IonEventType.CONTAINER_END) {
-        this.endContainer(
-          currentContainer.pop()!,
-          currentContainerIndex.pop()!
-        );
-      } else if (
-        tempEvent.eventType === IonEventType.SCALAR ||
-        tempEvent.eventType === IonEventType.STREAM_END
-      ) {
-        this.eventStream.push(tempEvent);
-      } else {
-        throw new Error("Unexpected eventType: " + tempEvent.eventType);
-      }
-      this.reader.stepOut();
-    }
-  }
-
-  private marshalEvent(): IonEvent {
-    let currentEvent = {};
-    for (let tid: IonType | null; (tid = this.reader.next()); ) {
-      let fieldName = this.reader.fieldName();
-      if (fieldName && currentEvent[fieldName] !== undefined)
-        throw new Error("Repeated event field: " + fieldName);
-      switch (fieldName) {
-        case "event_type": {
-          currentEvent[fieldName] = this.reader.stringValue();
-          break;
-        }
-
-        case "ion_type": {
-          currentEvent[fieldName] = this.parseIonType();
-          break;
-        }
-
-        case "field_name": {
-          currentEvent[fieldName] = this.reader.stringValue();
-          break;
-        }
-
-        case "annotations": {
-          currentEvent[fieldName] = this.parseAnnotations();
-          break;
-        }
-
-        case "value_text": {
-          let tempString: string = this.reader.stringValue()!;
-          if (tempString.substr(0, 5) === "$ion_")
-            tempString = "$ion_user_value::" + tempString;
-          let tempReader: Reader = makeReader(tempString);
-          tempReader.next();
-          let tempValue = tempReader.value();
-          currentEvent["isNull"] = tempReader.isNull();
-          currentEvent[fieldName] = tempValue;
-          break;
-        }
-
-        case "value_binary": {
-          currentEvent[fieldName] = this.parseBinaryValue();
-          break;
-        }
-
-        case "imports": {
-          currentEvent[fieldName] = this.parseImports();
-          break;
-        }
-
-        case "depth": {
-          currentEvent[fieldName] = this.reader.numberValue();
-          break;
-        }
-
-        default:
-          throw new Error("Unexpected event field: " + fieldName);
-      }
-    }
-    let eventType: IonEventType;
-    switch (currentEvent["event_type"]) {
-      case "CONTAINER_START":
-        eventType = IonEventType.CONTAINER_START;
-        break;
-      case "STREAM_END":
-        eventType = IonEventType.STREAM_END;
-        break;
-      case "CONTAINER_END":
-        eventType = IonEventType.CONTAINER_END;
-        break;
-      case "SCALAR":
-        eventType = IonEventType.SCALAR;
-        break;
-      case "SYMBOL_TABLE":
-        throw new Error("Symbol tables unsupported");
-    }
-    let fieldname =
-      currentEvent["field_name"] !== undefined
-        ? currentEvent["field_name"]
-        : null;
-    if (!currentEvent["annotations"]) currentEvent["annotations"] = [];
-
-    let textEvent = this.eventFactory.makeEvent(
-      eventType!,
-      currentEvent["ion_type"],
-      fieldname,
-      currentEvent["depth"],
-      currentEvent["annotations"],
-      currentEvent["isNull"],
-      currentEvent["value_text"]
-    );
-
-    if (eventType! === IonEventType.SCALAR) {
-      let binaryEvent = this.eventFactory.makeEvent(
-        eventType,
-        currentEvent["ion_type"],
-        fieldname,
-        currentEvent["depth"],
-        currentEvent["annotations"],
-        currentEvent["isNull"],
-        currentEvent["value_binary"]
-      );
-      if (!textEvent.equals(binaryEvent)) {
-        throw new Error(
-          `Text event ${currentEvent["value_text"]} does not equal binary event ${currentEvent["value_binary"]}`
-        );
-      }
-    }
-    return textEvent;
-  }
-
-  private parseIonType(): IonType {
-    let input: string = this.reader.stringValue()!.toLowerCase();
-    switch (input) {
-      case "null": {
-        return IonTypes.NULL;
-      }
-      case "bool": {
-        return IonTypes.BOOL;
-      }
-      case "int": {
-        return IonTypes.INT;
-      }
-      case "float": {
-        return IonTypes.FLOAT;
-      }
-      case "decimal": {
-        return IonTypes.DECIMAL;
-      }
-      case "timestamp": {
-        return IonTypes.TIMESTAMP;
-      }
-      case "symbol": {
-        return IonTypes.SYMBOL;
-      }
-      case "string": {
-        return IonTypes.STRING;
-      }
-      case "clob": {
-        return IonTypes.CLOB;
-      }
-      case "blob": {
-        return IonTypes.BLOB;
-      }
-      case "list": {
-        return IonTypes.LIST;
-      }
-      case "sexp": {
-        return IonTypes.SEXP;
-      }
-      case "struct": {
-        return IonTypes.STRUCT;
-      }
-      default: {
-        throw new Error("i: " + input);
-      }
-    }
-  }
-
-  private parseAnnotations(): string[] {
-    let annotations: string[] = [];
-    if (this.reader.isNull()) {
-      return annotations;
-    } else {
-      this.reader.stepIn();
-      for (let tid; (tid = this.reader.next()); ) {
-        annotations.push(this.reader.stringValue()!);
-      }
-      this.reader.stepOut();
-      return annotations;
-    }
-  }
-
-  private parseBinaryValue(): any {
-    //convert list of ints to array of bytes and pass the currentBuffer to a binary reader, generate value from factory.
-    //start with a null check
-    if (this.reader.isNull()) return null;
-    let numBuffer: number[] = [];
-    this.reader.stepIn();
-    let tid: IonType | null = this.reader.next();
-    while (tid) {
-      numBuffer.push(this.reader.numberValue()!);
-      tid = this.reader.next();
-    }
-    this.reader.stepOut();
-    let tempReader: Reader = makeReader(numBuffer);
-    tempReader.next();
-    return tempReader.value();
-  }
-
-  private parseImports(): any {
-    //TODO needed for symboltoken support.
-    return this.reader.value();
-  }
-}
diff --git a/src/dom/README.md b/src/dom/README.md
index 79d7771..3fbc0a1 100644
--- a/src/dom/README.md
+++ b/src/dom/README.md
@@ -3,13 +3,13 @@
 While the Ion streaming `Reader` API is more efficient, the Ion DOM `Value` API is often
 simpler for developers to work with.
 
-* [`ion.load()`](#ionload)
-* [`ion.loadAll()`](#ionloadall)
-* [`ion.dom.Value`](#iondomvalue)
-* [`ion.dom.Value.from()`](#iondomvaluefrom)
-* [`ion.dumpBinary()`, `ion.dumpText()`, and `ion.dumpPrettyText()`](#iondumpbinary-iondumptext-and-iondumpprettytext)
-* [`ion.dom` Data Types](#iondom-data-types)
-* [Testing Equality](#testing-equality)
+- [`ion.load()`](#ionload)
+- [`ion.loadAll()`](#ionloadall)
+- [`ion.dom.Value`](#iondomvalue)
+- [`ion.dom.Value.from()`](#iondomvaluefrom)
+- [`ion.dumpBinary()`, `ion.dumpText()`, and `ion.dumpPrettyText()`](#iondumpbinary-iondumptext-and-iondumpprettytext)
+- [`ion.dom` Data Types](#iondom-data-types)
+- [Testing Equality](#testing-equality)
 
 ### `ion.load()`
 
@@ -17,13 +17,14 @@ To get started, try reading some Ion data into memory by calling `ion.load()` an
 either a `string` or `Uint8Array` containing some serialized Ion data. This function will return
 a JavaScript object representation of the first value it encounters, including any nested values.
 
-Each Ion DOM data type [extends a native JavaScript type](#iondom-data-types), which means 
+Each Ion DOM data type [extends a native JavaScript type](#iondom-data-types), which means
 that in most cases you can treat values returned by `ion.load()` the same way you would
-treat the standard JavaScript equivalent. 
+treat the standard JavaScript equivalent.
 
 #### Integer example
+
 ```javascript
-let int = ion.load('7');
+let int = ion.load("7");
 assert.equal(int, 7);
 assert.equal(int + 1, 8);
 assert.equal(int ** 2, 49);
@@ -31,6 +32,7 @@ assert.equal(int / 7, 1);
 ```
 
 #### String example
+
 ```javascript
 let string = ion.load('"Hello, World!"');
 if (string.match(/hello/i)) {
@@ -40,19 +42,23 @@ if (string.match(/hello/i)) {
 ```
 
 #### List example
+
 ```javascript
-let list = ion.load('[1, 2, 3]');
+let list = ion.load("[1, 2, 3]");
 let sum = list.reduce((subTotal, value) => subTotal + value, 0);
 assert.equal(sum, 6);
 ```
 
 #### Struct example
+
 ```javascript
-let struct = ion.load('{name: {first: "John", middle: "Jacob", last: "Jingleheimer-Schmidt"}, age: 41}');
+let struct = ion.load(
+  '{name: {first: "John", middle: "Jacob", last: "Jingleheimer-Schmidt"}, age: 41}'
+);
 assert.equal(struct.name.middle, "Jacob");
 assert.equal(struct.age, 41);
 for (let [fieldName, value] of struct) {
-  console.log(fieldName + ': ' + value);
+  console.log(fieldName + ": " + value);
 }
 ```
 
@@ -62,7 +68,7 @@ If your Ion data source contains multiple top-level values, you can use `ion.loa
 eagerly read them all into an array.
 
 ```javascript
-import * as ion from 'ion-js';
+import * as ion from "ion-js";
 
 let values = ion.loadAll('6 "Hello" true');
 
@@ -76,17 +82,18 @@ assert.isTrue(values[2].booleanValue());
 Values returned by `load()` implement the `ion.dom.Value` interface, which provides explicit methods for
 indexing into Ion values as well as for converting them to native JS representations. While this API is more verbose
 than the JavaScript API demonstrated previously, it offers more type safety and is intended to be the interface used
-in TypeScript code. 
+in TypeScript code.
 
 #### Integer example
+
 ```typescript
 // `i` is a `dom.Integer`, which implements the `Value` interface.
-let i: Value = ion.load('7');
+let i: Value = ion.load("7");
 
 // The TypeScript compiler won't allow a direct comparison of a dom.Integer and a JS number, so
-// we'll need to down-convert our Ion value before comparing them. We can do this either by calling 
+// we'll need to down-convert our Ion value before comparing them. We can do this either by calling
 // the Value interface's numberValue() method:
-assert.equal(i.numberValue(), 7); 
+assert.equal(i.numberValue(), 7);
 // or by using JavaScript's unary `+` operator, which is more concise but less explicit:
 assert.equal(+i, 7);
 
@@ -95,64 +102,67 @@ assert.throws(() => i.stringValue());
 ```
 
 #### String example
+
 ```typescript
 // `s` is a `dom.String`, which implements the `Value` interface.
 let s: Value = ion.load('"foo"');
 
 // The TypeScript compiler won't allow a direct comparison of a dom.String and a JS string, so
-// we'll need to down-convert our Ion value before comparing them. We can do this either by calling 
+// we'll need to down-convert our Ion value before comparing them. We can do this either by calling
 // the Value interface's stringValue() method:
-assert.equal(s.stringValue(), "foo"); 
+assert.equal(s.stringValue(), "foo");
 // or concatenate our dom.String with the empty string, which is more concise but less explicit:
-assert.equal(''+s, "foo");
+assert.equal("" + s, "foo");
 
 // dom.String is not a numeric value, so there is no number value associated with it.
 assert.throws(() => s.numberValue());
 ```
 
 #### List example
+
 ```typescript
 // `l` is a `dom.List`, which implements the `Value` interface.
-let l: Value = ion.load('[0, 2, 4, 6, 8, 10]');
+let l: Value = ion.load("[0, 2, 4, 6, 8, 10]");
 
 // We can use the `get()` method to index into the Value
 assert.equal(l.get(2), 4);
 
 // iteration example using the `elements()` method
 for (let value of l.elements()) {
-    assert.isTrue(value % 2 == 0);
+  assert.isTrue(value % 2 == 0);
 }
 ```
 
 #### Struct example
+
 ```typescript
 // Ion struct
 let person: Value = ion.load(
-  '{' +
-      'name: {' +
-          'first: "John", ' +
-          'middle: "Jacob", ' +
-          'last: "Jingleheimer-Schmidt",' +
-     '},' +
-     'age: 41' +
-  '}'
+  "{" +
+    "name: {" +
+    'first: "John", ' +
+    'middle: "Jacob", ' +
+    'last: "Jingleheimer-Schmidt",' +
+    "}," +
+    "age: 41" +
+    "}"
 )!;
 
 // The get() method can also be used for indexing into structs
-assert.equal(person.get('age'), 41);
+assert.equal(person.get("age"), 41);
 
 // If the data is nested, you can pass multiple keys into get() to specify a complete path:
-assert.equal(person.get('name', 'middle'), 'Jacob');
+assert.equal(person.get("name", "middle"), "Jacob");
 
 // Iteration example using the `fields()` method
 for (let [fieldName, value] of person.fields()) {
-    console.log(fieldName + ': ' + value);
+  console.log(fieldName + ": " + value);
 }
 ```
 
 ### `ion.dom.Value.from()`
 
-You can upgrade a JS value to an Ion value using the `from()` method: 
+You can upgrade a JS value to an Ion value using the `from()` method:
 
 ```typescript
 let i = ion.dom.Value.from(7);
@@ -162,7 +172,7 @@ assert.equal(i.numberValue(), 7);
 let s = ion.dom.Value.from('"foo"', ["bar", "baz"]); // from() accepts an optional list of annotations
 assert.equal(s.getType(), IonTypes.STRING);
 assert.equal(s.stringValue(), "foo");
-assert.deepEqual(s.getAnnotations(), ['bar', 'baz']);
+assert.deepEqual(s.getAnnotations(), ["bar", "baz"]);
 ```
 
 ## `ion.dumpBinary()`, `ion.dumpText()`, and `ion.dumpPrettyText()`
@@ -170,14 +180,15 @@ assert.deepEqual(s.getAnnotations(), ['bar', 'baz']);
 As a complement to `ion.load()`, Ion provides a collection of convenience methods that make it easy to produce
 serialized Ion text or binary data from a given in-memory object:
 
-* `ion.dumpBinary()` writes its parameters to a binary Ion stream and returns a `Uint8Array` containing the 
+- `ion.dumpBinary()` writes its parameters to a binary Ion stream and returns a `Uint8Array` containing the
   serialized bytes.
-* `ion.dumpText()` writes its parameters to a compact text Ion stream and returns a `string` containing the
+- `ion.dumpText()` writes its parameters to a compact text Ion stream and returns a `string` containing the
   serialized data.
-* `ion.dumpPrettyText()` writes its parameters to a text Ion stream that has been generously spaced for human
+- `ion.dumpPrettyText()` writes its parameters to a text Ion stream that has been generously spaced for human
   readability and returns a `string` containing the serialized data.
 
 ### ion.dumpBinary() round-tripping example
+
 ```javascript
 // Write the number 7 to a binary Ion stream and store the bytes in `data`
 let data = ion.dumpBinary(7);
@@ -188,44 +199,46 @@ assert.equal(i, 7);
 ```
 
 ### ion.dumpText() vs ion.dumpPrettyText() example
+
 ```javascript
 let ionText = ion.dumpText([1, 2, 3]);
 let ionPrettyText = ion.dumpPrettyText([1, 2, 3]);
 
-assert.equal('[1,2,3]', ionText);
-assert.equal('[\n  1,\n  2,\n  3\n]', ionPrettyText);
+assert.equal("[1,2,3]", ionText);
+assert.equal("[\n  1,\n  2,\n  3\n]", ionPrettyText);
 ```
 
 ## `ion.dom` Data Types
 
-The Ion DOM data types each extend a native JavaScript type: 
-
-Ion DOM Type | Extended JS Type
----|:---: 
-`ion.dom.Null` | `Object`
-`ion.dom.Boolean` | `Boolean`
-`ion.dom.Integer` | `Number`
-`ion.dom.Float` | `Number`
-`ion.dom.Decimal` | `Number`
-`ion.dom.Timestamp` | `Date`
-`ion.dom.Symbol` | `String`
-`ion.dom.String` | `String`
-`ion.dom.Clob` | `Uint8Array`
-`ion.dom.Blob` | `Uint8Array`
-`ion.dom.List` | `Array`
-`ion.dom.SExpression` | `Array`
-`ion.dom.Struct` | `Object`
+The Ion DOM data types each extend a native JavaScript type:
+
+| Ion DOM Type          | Extended JS Type |
+| --------------------- | :--------------: |
+| `ion.dom.Null`        |     `Object`     |
+| `ion.dom.Boolean`     |    `Boolean`     |
+| `ion.dom.Integer`     |     `Number`     |
+| `ion.dom.Float`       |     `Number`     |
+| `ion.dom.Decimal`     |     `Number`     |
+| `ion.dom.Timestamp`   |      `Date`      |
+| `ion.dom.Symbol`      |     `String`     |
+| `ion.dom.String`      |     `String`     |
+| `ion.dom.Clob`        |   `Uint8Array`   |
+| `ion.dom.Blob`        |   `Uint8Array`   |
+| `ion.dom.List`        |     `Array`      |
+| `ion.dom.SExpression` |     `Array`      |
+| `ion.dom.Struct`      |     `Object`     |
 
 ### Testing Equality
 
 Note that the JS types being extended are "[wrapper objects](https://developer.mozilla.org/en-US/docs/Glossary/Primitive#Primitive_wrapper_objects_in_JavaScript)"
-that box the primitive types themselves. This matters when testing for equality; JavaScript's strict definition of equality 
+that box the primitive types themselves. This matters when testing for equality; JavaScript's strict definition of equality
 (used by the `===` operator) takes `Object` identity into account and so will return `false` when comparing two different
 wrapper objects representing the same value.
 
-#### `String` example 
+#### `String` example
 
 The code below demonstrates this behavior using the primitive `string` type and its wrapper object, `String`:
+
 ```javascript
 let string = "foo"; // Primitive string
 let boxedString = new String("foo"); // String wrapper object
@@ -234,11 +247,12 @@ let boxedString2 = new String("foo"); // Second wrapper object with the same val
 assert.isTrue(string == boxedString);
 assert.isFalse(string === boxedString);
 assert.isFalse(boxedString == boxedString2);
-assert.isTrue(string === ''+boxedString);
+assert.isTrue(string === "" + boxedString);
 assert.isFalse(boxedString === boxedString2);
 ```
 
 Ion `dom.Value` instances exhibit the same behavior:
+
 ```javascript
 let string = "foo"; // Primitive string
 let ionString = ion.dom.Value.from("foo"); // ion.dom.String instance
@@ -246,7 +260,7 @@ let ionString2 = ion.dom.Value.from("foo"); // Second ion.dom.String instance wi
 
 assert.isTrue(string == ionString);
 assert.isFalse(string === ionString);
-assert.isTrue(string === ''+ionString);
+assert.isTrue(string === "" + ionString);
 assert.isTrue(string === ionString.stringValue());
 assert.isFalse(ionString === ionString2);
-``` 
\ No newline at end of file
+```
diff --git a/src/events/EventStreamError.ts b/src/events/EventStreamError.ts
new file mode 100644
index 0000000..7dfd8a3
--- /dev/null
+++ b/src/events/EventStreamError.ts
@@ -0,0 +1,20 @@
+import { IonEvent } from "../Ion";
+
+export class EventStreamError extends Error {
+  type: string;
+  index: number;
+  eventstream: IonEvent[];
+
+  constructor(
+    type: string,
+    message: string,
+    index: number,
+    eventstream: IonEvent[]
+  ) {
+    super();
+    this.type = type;
+    this.index = index;
+    this.message = message;
+    this.eventstream = eventstream;
+  }
+}
diff --git a/src/IonEvent.ts b/src/events/IonEvent.ts
similarity index 58%
rename from src/IonEvent.ts
rename to src/events/IonEvent.ts
index 50c007c..014f9ee 100644
--- a/src/IonEvent.ts
+++ b/src/events/IonEvent.ts
@@ -13,17 +13,19 @@
  * permissions and limitations under the License.
  */
 
-import { IonTypes } from "./IonTypes";
-import { Decimal } from "./IonDecimal";
-import { Timestamp } from "./IonTimestamp";
-import { IonType } from "./IonType";
-import { Writer } from "./IonWriter";
-import { TextWriter } from "./IonTextWriter";
-import { Writeable } from "./IonWriteable";
-import { BinaryWriter } from "./IonBinaryWriter";
-import { defaultLocalSymbolTable } from "./IonLocalSymbolTable";
-import { decodeUtf8 } from "./IonUnicode";
+import { IonTypes } from "../IonTypes";
+import { Decimal } from "../IonDecimal";
+import { Timestamp } from "../IonTimestamp";
+import { IonType } from "../IonType";
+import { Writer } from "../IonWriter";
+import { TextWriter } from "../IonTextWriter";
+import { Writeable } from "../IonWriteable";
+import { BinaryWriter } from "../IonBinaryWriter";
+import { defaultLocalSymbolTable } from "../IonLocalSymbolTable";
+import { decodeUtf8 } from "../IonUnicode";
 import JSBI from "jsbi";
+import { ComparisonResultType } from "../ComparisonResult";
+import { ComparisonResult } from "../ComparisonResult";
 
 export enum IonEventType {
   SCALAR = 0,
@@ -45,6 +47,8 @@ export interface IonEvent {
 
   equals(expected: IonEvent): boolean;
 
+  compare(expected: IonEvent): ComparisonResult;
+
   writeIonValue(writer: Writer): void;
 }
 
@@ -74,7 +78,7 @@ abstract class AbstractIonEvent implements IonEvent {
 
   abstract writeIonValue(writer: Writer): void;
 
-  abstract valueEquals(expected: IonEvent): boolean;
+  abstract valueCompare(expected: IonEvent): ComparisonResult;
 
   write(writer: Writer) {
     writer.stepIn(IonTypes.STRUCT);
@@ -86,7 +90,10 @@ abstract class AbstractIonEvent implements IonEvent {
     }
     if (this.fieldName !== null && this.fieldName !== undefined) {
       writer.writeFieldName("field_name");
+      writer.stepIn(IonTypes.STRUCT);
+      writer.writeFieldName("text");
       writer.writeString(this.fieldName);
+      writer.stepOut();
     }
     if (this.annotations !== null) {
       writer.writeFieldName("annotations");
@@ -113,7 +120,10 @@ abstract class AbstractIonEvent implements IonEvent {
     }
     writer.stepIn(IonTypes.LIST);
     for (let i = 0; i < this.annotations.length; i++) {
+      writer.stepIn(IonTypes.STRUCT);
+      writer.writeFieldName("text");
       writer.writeString(this.annotations[i]);
+      writer.stepOut();
     }
     writer.stepOut();
   }
@@ -169,23 +179,74 @@ abstract class AbstractIonEvent implements IonEvent {
   }
 
   equals(expected: IonEvent): boolean {
-    return (
-      this.eventType === expected.eventType &&
-      this.ionType === expected.ionType &&
-      this.fieldName === expected.fieldName &&
-      this.depth === expected.depth &&
-      this.annotationEquals(expected.annotations) &&
-      this.valueEquals(expected)
-    );
+    return this.compare(expected).result == ComparisonResultType.EQUAL;
   }
 
-  annotationEquals(expectedAnnotations: string[]): boolean {
-    if (this.annotations === expectedAnnotations) return true;
-    if (this.annotations.length !== expectedAnnotations.length) return false;
+  /**
+   *  compares events for equivalence and generate comparison result
+   */
+  compare(expected: IonEvent): ComparisonResult {
+    if (this.eventType !== expected.eventType) {
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Event types don't match"
+      );
+    }
+    if (this.ionType !== expected.ionType) {
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Ion types don't match " +
+          this.ionType?.name +
+          " vs. " +
+          expected.ionType?.name
+      );
+    }
+    if (this.fieldName !== expected.fieldName) {
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Field names don't match " +
+          this.fieldName +
+          " vs. " +
+          expected.fieldName
+      );
+    }
+    if (this.depth !== expected.depth) {
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Event depths don't match " + this.depth + " vs. " + expected.depth
+      );
+    }
+    let annotationResult = this.annotationCompare(expected.annotations);
+    if (annotationResult.result === ComparisonResultType.NOT_EQUAL)
+      return annotationResult;
+    let valueResult = this.valueCompare(expected);
+    if (valueResult.result === ComparisonResultType.NOT_EQUAL)
+      return valueResult;
+    return new ComparisonResult(ComparisonResultType.EQUAL);
+  }
+
+  annotationCompare(expectedAnnotations: string[]): ComparisonResult {
+    if (this.annotations === expectedAnnotations)
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    if (this.annotations.length !== expectedAnnotations.length)
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "annotations length don't match" +
+          this.annotations.length +
+          " vs. " +
+          expectedAnnotations.length
+      );
     for (let i = 0; i < this.annotations.length; i++) {
-      if (this.annotations[i] !== expectedAnnotations[i]) return false;
+      if (this.annotations[i] !== expectedAnnotations[i])
+        return new ComparisonResult(
+          ComparisonResultType.NOT_EQUAL,
+          "annotation value doesn't match" +
+            this.annotations[i] +
+            " vs. " +
+            expectedAnnotations[i]
+        );
     }
-    return true;
+    return new ComparisonResult(ComparisonResultType.EQUAL);
   }
 }
 
@@ -353,9 +414,12 @@ class IonNullEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, null);
   }
 
-  valueEquals(expected: IonNullEvent): boolean {
-    return (
-      expected instanceof IonNullEvent && this.ionValue === expected.ionValue
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (expected instanceof IonNullEvent && this.ionValue === expected.ionValue)
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
     );
   }
 
@@ -376,10 +440,15 @@ class IonIntEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonIntEvent): boolean {
-    return (
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (
       expected instanceof IonIntEvent &&
       JSBI.equal(this.ionValue, expected.ionValue)
+    )
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
     );
   }
 
@@ -400,9 +469,12 @@ class IonBoolEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonBoolEvent): boolean {
-    return (
-      expected instanceof IonBoolEvent && this.ionValue === expected.ionValue
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (expected instanceof IonBoolEvent && this.ionValue === expected.ionValue)
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
     );
   }
 
@@ -423,10 +495,17 @@ class IonFloatEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonFloatEvent): boolean {
-    if (isNaN(this.ionValue) && isNaN(expected.ionValue)) return true;
-    return (
-      expected instanceof IonFloatEvent && this.ionValue === expected.ionValue
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (isNaN(this.ionValue) && isNaN(expected.ionValue))
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    if (
+      expected instanceof IonFloatEvent &&
+      this.ionValue === expected.ionValue
+    )
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
     );
   }
 
@@ -447,10 +526,15 @@ class IonDecimalEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonDecimalEvent): boolean {
-    return (
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (
       expected instanceof IonDecimalEvent &&
       this.ionValue.equals(expected.ionValue)
+    )
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
     );
   }
 
@@ -472,12 +556,17 @@ class IonSymbolEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonSymbolEvent): boolean {
-    return (
-      expected instanceof IonSymbolEvent && this.ionValue === expected.ionValue
-    ); //will need to change when symboltokens are introduced.
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (
+      expected instanceof IonSymbolEvent &&
+      this.ionValue === expected.ionValue
+    )
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
+    );
   }
-
   writeIonValue(writer: Writer): void {
     writer.writeSymbol(this.ionValue); //if symboltokens text is unknown we will need to write out symboltable
   }
@@ -495,9 +584,15 @@ class IonStringEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonStringEvent): boolean {
-    return (
-      expected instanceof IonStringEvent && this.ionValue === expected.ionValue
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (
+      expected instanceof IonStringEvent &&
+      this.ionValue === expected.ionValue
+    )
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
     );
   }
 
@@ -518,10 +613,15 @@ class IonTimestampEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonTimestampEvent): boolean {
-    return (
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (
       expected instanceof IonTimestampEvent &&
       this.ionValue.equals(expected.ionValue)
+    )
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
     );
   }
 
@@ -542,13 +642,22 @@ class IonBlobEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonBlobEvent): boolean {
-    if (!(expected instanceof IonBlobEvent)) return false;
-    if (this.ionValue.length !== expected.ionValue.length) return false;
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (!(expected instanceof IonBlobEvent))
+      return new ComparisonResult(ComparisonResultType.NOT_EQUAL);
+    if (this.ionValue.length !== expected.ionValue.length)
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Blob length don't match"
+      );
     for (let i = 0; i < this.ionValue.length; i++) {
-      if (this.ionValue[i] !== expected.ionValue[i]) return false;
+      if (this.ionValue[i] !== expected.ionValue[i])
+        return new ComparisonResult(
+          ComparisonResultType.NOT_EQUAL,
+          this.ionValue[i] + " vs. " + expected.ionValue[i]
+        );
     }
-    return true;
+    return new ComparisonResult(ComparisonResultType.EQUAL);
   }
 
   writeIonValue(writer: Writer): void {
@@ -568,13 +677,17 @@ class IonClobEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, ionValue);
   }
 
-  valueEquals(expected: IonClobEvent): boolean {
-    //can there be byte padding?
-    if (!(expected instanceof IonClobEvent)) return false;
+  valueCompare(expected: IonEvent): ComparisonResult {
+    if (!(expected instanceof IonClobEvent))
+      return new ComparisonResult(ComparisonResultType.NOT_EQUAL);
     for (let i = 0; i < this.ionValue.length; i++) {
-      if (this.ionValue[i] !== expected.ionValue[i]) return false;
+      if (this.ionValue[i] !== expected.ionValue[i])
+        return new ComparisonResult(
+          ComparisonResultType.NOT_EQUAL,
+          this.ionValue[i] + " vs. " + expected.ionValue[i]
+        );
     }
-    return true;
+    return new ComparisonResult(ComparisonResultType.EQUAL);
   }
 
   writeIonValue(writer: Writer): void {
@@ -593,7 +706,7 @@ abstract class AbsIonContainerEvent extends AbstractIonEvent {
     super(eventType, ionType, fieldName, annotations, depth, null);
   }
 
-  abstract valueEquals(expected: AbsIonContainerEvent);
+  abstract valueCompare(expected: AbsIonContainerEvent);
 
   writeIonValue(writer: Writer): void {
     //no op;
@@ -613,39 +726,68 @@ class IonStructEvent extends AbsIonContainerEvent {
   }
 
   //two way equivalence between the structEvents, they must have the exact same number of equivalent elements.
-  valueEquals(expected: IonStructEvent): boolean {
-    return (
-      expected instanceof IonStructEvent &&
-      this.ionValue.length === expected.ionValue.length &&
-      this.structsEqual(this.ionValue, expected.ionValue)
-    );
+  valueCompare(expected: IonStructEvent): ComparisonResult {
+    if (!(expected instanceof IonStructEvent)) {
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Event types don't match"
+      );
+    }
+    let container = this.ionValue == null ? [] : this.ionValue;
+    let expectedContainer = expected.ionValue == null ? [] : expected.ionValue;
+    if (container.length !== expectedContainer.length)
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Struct length don't match"
+      );
+    return this.structsCompare(container, expectedContainer);
   }
 
   //for each actual ionEvent, searches for an equivalent expected ionEvent,
   //equivalent pairings remove the paired expectedEvent from the search space.
-  structsEqual(
+  structsCompare(
     actualEvents: AbstractIonEvent[],
     expectedEvents: AbstractIonEvent[]
-  ): boolean {
-    let matchFound: boolean = true;
+  ): ComparisonResult {
+    let matchFound: ComparisonResult = new ComparisonResult(
+      ComparisonResultType.EQUAL
+    );
     let paired: boolean[] = new Array<boolean>(expectedEvents.length);
     for (let i: number = 0; matchFound && i < actualEvents.length; i++) {
-      matchFound = false;
-      for (let j: number = 0; !matchFound && j < expectedEvents.length; j++) {
+      matchFound.result = ComparisonResultType.NOT_EQUAL;
+      for (
+        let j: number = 0;
+        matchFound.result == ComparisonResultType.NOT_EQUAL &&
+        j < expectedEvents.length;
+        j++
+      ) {
         if (!paired[j]) {
           let child = actualEvents[i];
           let expectedChild = expectedEvents[j];
-          matchFound = child.equals(expectedChild);
-          if (matchFound) paired[j] = true;
-          if (matchFound && child.eventType === IonEventType.CONTAINER_START) {
-            for (let k = j + 1; k < expectedChild.ionValue.length; k++) {
-              paired[k] = true;
+          matchFound = child.compare(expectedChild);
+          if (matchFound.result == ComparisonResultType.EQUAL) paired[j] = true;
+          if (
+            matchFound.result == ComparisonResultType.EQUAL &&
+            child.eventType === IonEventType.CONTAINER_START
+          ) {
+            for (let k = 0; k < expectedChild.ionValue.length; k++) {
+              paired[k + j + 1] = true;
             }
             i += child.ionValue.length;
           }
         }
       }
     }
+    // set matchFound to the first pair that didn't find a matching field if any
+    for (let i: number = 0; i < paired.length; i++) {
+      if (!paired[i]) {
+        matchFound = new ComparisonResult(
+          ComparisonResultType.NOT_EQUAL,
+          "Didn't find matching field for " + expectedEvents[i].fieldName
+        );
+        break;
+      }
+    }
     return matchFound;
   }
 }
@@ -661,20 +803,36 @@ class IonListEvent extends AbsIonContainerEvent {
     super(eventType, ionType, fieldName, annotations, depth);
   }
 
-  valueEquals(expected: IonListEvent): boolean {
-    if (!(expected instanceof IonListEvent)) return false;
-    let container = this.ionValue;
-    let expectedContainer = expected.ionValue;
-    if (container.length !== expectedContainer.length) return false;
+  valueCompare(expected: IonListEvent): ComparisonResult {
+    if (!(expected instanceof IonListEvent))
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Event types don't match"
+      );
+    let container = this.ionValue == null ? [] : this.ionValue;
+    let expectedContainer = expected.ionValue == null ? [] : expected.ionValue;
+    if (container.length !== expectedContainer.length)
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "List length don't match"
+      );
     for (let i: number = 0; i < container.length; i++) {
       let child = container[i];
-      if (!child.equals(expectedContainer[i])) {
-        return false;
+      if (
+        child.compare(expectedContainer[i]).result ==
+        ComparisonResultType.NOT_EQUAL
+      ) {
+        return new ComparisonResult(
+          ComparisonResultType.NOT_EQUAL,
+          child.ionValue + " vs. " + expectedContainer[i].ionValue,
+          i + 1,
+          i + 1
+        );
       } else if (child.eventType === IonEventType.CONTAINER_START) {
         i += child.ionValue.length;
       }
     }
-    return true;
+    return new ComparisonResult(ComparisonResultType.EQUAL);
   }
 }
 
@@ -689,20 +847,29 @@ class IonSexpEvent extends AbsIonContainerEvent {
     super(eventType, ionType, fieldName, annotations, depth);
   }
 
-  valueEquals(expected: IonSexpEvent): boolean {
-    if (!(expected instanceof IonSexpEvent)) return false;
-    let container = this.ionValue;
-    let expectedContainer = expected.ionValue;
-    if (container.length !== expectedContainer.length) return false;
+  valueCompare(expected: IonSexpEvent): ComparisonResult {
+    if (!(expected instanceof IonSexpEvent))
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "Event types don't match"
+      );
+    let container = this.ionValue == null ? [] : this.ionValue;
+    let expectedContainer = expected.ionValue == null ? [] : expected.ionValue;
+    if (container.length !== expectedContainer.length)
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "S-expression length don't match"
+      );
     for (let i: number = 0; i < container.length; i++) {
       let child = container[i];
-      if (!child.equals(expectedContainer[i])) {
-        return false;
+      let eventResult = child.compare(expectedContainer[i]);
+      if (eventResult.result == ComparisonResultType.NOT_EQUAL) {
+        return eventResult;
       } else if (child.eventType === IonEventType.CONTAINER_START) {
         i += child.ionValue.length;
       }
     }
-    return true;
+    return new ComparisonResult(ComparisonResultType.EQUAL);
   }
 }
 
@@ -711,10 +878,18 @@ class IonEndEvent extends AbstractIonEvent {
     super(eventType, null, null, [], depth, undefined);
   }
 
-  valueEquals(expected: IonEndEvent) {
-    return (
-      expected instanceof IonEndEvent && this.ionValue === expected.ionValue
-    ); //should be null === null if they are both end events.
+  valueCompare(expected: IonEndEvent): ComparisonResult {
+    //should be null === null if they are both end events.
+    if (
+      expected instanceof IonEndEvent &&
+      this.ionValue === expected.ionValue
+    ) {
+      return new ComparisonResult(ComparisonResultType.EQUAL);
+    }
+    return new ComparisonResult(
+      ComparisonResultType.NOT_EQUAL,
+      this.ionValue + " vs. " + expected.ionValue
+    );
   }
 
   writeIonValue(writer: Writer): void {
diff --git a/src/events/IonEventStream.ts b/src/events/IonEventStream.ts
new file mode 100644
index 0000000..6391a6c
--- /dev/null
+++ b/src/events/IonEventStream.ts
@@ -0,0 +1,628 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { Reader } from "../IonReader";
+import { IonEvent, IonEventFactory, IonEventType } from "./IonEvent";
+import { Writer } from "../IonWriter";
+import { IonType } from "../IonType";
+import { IonTypes } from "../IonTypes";
+import { defaultLocalSymbolTable, makeReader, ReaderOctetBuffer } from "../Ion";
+import { BinaryReader } from "../IonBinaryReader";
+import { BinarySpan } from "../IonSpan";
+import { ComparisonResultType } from "../ComparisonResult";
+import { ComparisonResult } from "../ComparisonResult";
+import { EventStreamError } from "./EventStreamError";
+
+// constants to be used for EventStream error
+const READ = "READ";
+const WRITE = "WRITE";
+
+export class IonEventStream {
+  events: IonEvent[];
+  private reader: Reader;
+  private eventFactory: IonEventFactory;
+  isEventStream: boolean; // whether the reader has an event stream as input
+
+  constructor(reader: Reader) {
+    this.events = [];
+    this.reader = reader;
+    this.eventFactory = new IonEventFactory();
+    this.isEventStream = false;
+    this.generateStream();
+  }
+
+  writeEventStream(writer: Writer) {
+    writer.writeSymbol("$ion_event_stream");
+    for (let i: number = 0; i < this.events.length; i++) {
+      this.events[i].write(writer);
+    }
+  }
+
+  writeIon(writer: Writer) {
+    try {
+      let tempEvent: IonEvent;
+      let isEmbedded = false;
+      for (let indice: number = 0; indice < this.events.length; indice++) {
+        tempEvent = this.events[indice];
+        if (tempEvent.fieldName !== null) {
+          writer.writeFieldName(tempEvent.fieldName);
+        }
+        if (
+          (tempEvent.ionType == IonTypes.SEXP ||
+            tempEvent.ionType == IonTypes.LIST) &&
+          this.isEmbedded(tempEvent)
+        ) {
+          isEmbedded = true;
+        }
+        writer.setAnnotations(tempEvent.annotations);
+        switch (tempEvent.eventType) {
+          case IonEventType.SCALAR:
+            if (tempEvent.ionValue == null) {
+              writer.writeNull(tempEvent.ionType!);
+              return;
+            }
+            if (isEmbedded) {
+              writer.writeString(tempEvent.ionValue.toString());
+              break;
+            }
+            switch (tempEvent.ionType) {
+              case IonTypes.BOOL:
+                writer.writeBoolean(tempEvent.ionValue);
+                break;
+              case IonTypes.STRING:
+                writer.writeString(tempEvent.ionValue);
+                break;
+              case IonTypes.SYMBOL:
+                writer.writeSymbol(tempEvent.ionValue);
+                break;
+              case IonTypes.INT:
+                writer.writeInt(tempEvent.ionValue);
+                break;
+              case IonTypes.DECIMAL:
+                writer.writeDecimal(tempEvent.ionValue);
+                break;
+              case IonTypes.FLOAT:
+                writer.writeFloat64(tempEvent.ionValue);
+                break;
+              case IonTypes.NULL:
+                writer.writeNull(tempEvent.ionType);
+                break;
+              case IonTypes.TIMESTAMP:
+                writer.writeTimestamp(tempEvent.ionValue);
+                break;
+              case IonTypes.CLOB:
+                writer.writeClob(tempEvent.ionValue);
+                break;
+              case IonTypes.BLOB:
+                writer.writeBlob(tempEvent.ionValue);
+                break;
+              default:
+                throw new Error("unexpected type: " + tempEvent.ionType!.name);
+            }
+            break;
+          case IonEventType.CONTAINER_START:
+            writer.stepIn(tempEvent.ionType!);
+            break;
+          case IonEventType.CONTAINER_END:
+            if (isEmbedded) {
+              isEmbedded = false;
+            }
+            writer.stepOut();
+            break;
+          case IonEventType.STREAM_END:
+            break;
+          case IonEventType.SYMBOL_TABLE:
+            throw new Error("Symboltables unsupported.");
+          default:
+            throw new Error("Unexpected event type: " + tempEvent.eventType);
+        }
+      }
+      writer.close();
+    } catch (error) {
+      // This Error will be used by the test-driver to differentiate errors using error types.
+      throw new EventStreamError(
+        WRITE,
+        error.message,
+        this.events.length,
+        this.events
+      );
+    }
+  }
+
+  getEvents(): IonEvent[] {
+    return this.events;
+  }
+
+  equals(expected: IonEventStream): boolean {
+    return this.compare(expected).result == ComparisonResultType.EQUAL;
+  }
+
+  /**
+   *  compares eventstreams and generates comparison result
+   */
+  compare(expected: IonEventStream): ComparisonResult {
+    let actualIndex: number = 0;
+    let expectedIndex: number = 0;
+    if (this.events.length != expected.events.length) {
+      return new ComparisonResult(
+        ComparisonResultType.NOT_EQUAL,
+        "The event streams have different lengths"
+      );
+    }
+    while (
+      actualIndex < this.events.length &&
+      expectedIndex < expected.events.length
+    ) {
+      let actualEvent = this.events[actualIndex];
+      let expectedEvent = expected.events[expectedIndex];
+      if (actualEvent.eventType === IonEventType.SYMBOL_TABLE) actualIndex++;
+      if (expectedEvent.eventType === IonEventType.SYMBOL_TABLE)
+        expectedIndex++;
+      if (
+        actualEvent.eventType === IonEventType.SYMBOL_TABLE ||
+        expectedEvent.eventType === IonEventType.SYMBOL_TABLE
+      )
+        continue;
+      switch (actualEvent.eventType) {
+        case IonEventType.SCALAR: {
+          let eventResult = actualEvent.compare(expectedEvent);
+          if (eventResult.result == ComparisonResultType.NOT_EQUAL) {
+            eventResult.actualIndex = actualIndex;
+            eventResult.expectedIndex = expectedIndex;
+            return eventResult;
+          }
+          break;
+        }
+        case IonEventType.CONTAINER_START: {
+          let eventResult = actualEvent.compare(expectedEvent);
+          if (eventResult.result == ComparisonResultType.NOT_EQUAL) {
+            actualIndex += eventResult.actualIndex;
+            expectedIndex += eventResult.expectedIndex;
+
+            eventResult.actualIndex = actualIndex;
+            eventResult.expectedIndex = expectedIndex;
+            return eventResult;
+          } else {
+            if (
+              actualEvent.ionValue !== null &&
+              expectedEvent.ionValue !== null
+            ) {
+              actualIndex = actualIndex + actualEvent.ionValue.length;
+              expectedIndex = expectedIndex + expectedEvent.ionValue.length;
+            }
+          }
+          break;
+        }
+        case IonEventType.CONTAINER_END:
+        case IonEventType.STREAM_END: {
+          //no op
+          break;
+        }
+        default: {
+          throw new Error("Unexpected event type: " + actualEvent.eventType);
+        }
+      }
+      actualIndex++;
+      expectedIndex++;
+    }
+    return new ComparisonResult(ComparisonResultType.EQUAL);
+  }
+
+  isEmbedded(event: IonEvent): boolean {
+    if (event.annotations[0] === "embedded_documents") {
+      return true;
+    }
+    return false;
+  }
+
+  private generateStream(): void {
+    try {
+      let tid: IonType | null = this.reader.next();
+      if (
+        tid === IonTypes.SYMBOL &&
+        this.reader.stringValue() === "$ion_event_stream"
+      ) {
+        this.marshalStream();
+        this.isEventStream = true;
+        return;
+      }
+      let currentContainer: IonEvent[] = [];
+      let currentContainerIndex: number[] = [];
+      while (true) {
+        if (this.reader.isNull()) {
+          this.events.push(
+            this.eventFactory.makeEvent(
+              IonEventType.SCALAR,
+              tid!,
+              this.reader.fieldName(),
+              this.reader.depth(),
+              this.reader.annotations(),
+              true,
+              this.reader.value()
+            )
+          );
+        } else {
+          switch (tid) {
+            case IonTypes.LIST:
+            case IonTypes.SEXP:
+            case IonTypes.STRUCT: {
+              let containerEvent = this.eventFactory.makeEvent(
+                IonEventType.CONTAINER_START,
+                tid,
+                this.reader.fieldName(),
+                this.reader.depth(),
+                this.reader.annotations(),
+                false,
+                null
+              );
+              this.events.push(containerEvent);
+              currentContainer.push(containerEvent);
+              currentContainerIndex.push(this.events.length);
+              this.reader.stepIn();
+              break;
+            }
+            case null: {
+              if (this.reader.depth() === 0) {
+                this.events.push(
+                  this.eventFactory.makeEvent(
+                    IonEventType.STREAM_END,
+                    IonTypes.NULL,
+                    null,
+                    this.reader.depth(),
+                    [],
+                    false,
+                    undefined
+                  )
+                );
+                return;
+              } else {
+                this.reader.stepOut();
+                this.endContainer(
+                  currentContainer.pop()!,
+                  currentContainerIndex.pop()!
+                );
+              }
+              break;
+            }
+            default: {
+              this.events.push(
+                this.eventFactory.makeEvent(
+                  IonEventType.SCALAR,
+                  tid,
+                  this.reader.fieldName(),
+                  this.reader.depth(),
+                  this.reader.annotations(),
+                  false,
+                  this.reader.value()
+                )
+              );
+              break;
+            }
+          }
+        }
+        tid = this.reader.next();
+      }
+    } catch (error) {
+      // This Error will be used by the test-driver to differentiate errors using error types.
+      throw new EventStreamError(
+        READ,
+        error.message,
+        this.events.length,
+        this.events
+      );
+    }
+  }
+
+  private endContainer(thisContainer: IonEvent, thisContainerIndex: number) {
+    this.events.push(
+      this.eventFactory.makeEvent(
+        IonEventType.CONTAINER_END,
+        thisContainer.ionType!,
+        null,
+        thisContainer.depth,
+        [],
+        false,
+        null
+      )
+    );
+    thisContainer.ionValue = this.events.slice(
+      thisContainerIndex,
+      this.events.length
+    );
+  }
+
+  //(event_type: EventType, ion_type: IonType, field_name: SymbolToken, annotations: list<SymbolToken>, value_text: string, value_binary: list<byte>, imports: list<ImportDescriptor>, depth: int)
+  private marshalStream(): void {
+    this.events = [];
+    let currentContainer: IonEvent[] = [];
+    let currentContainerIndex: number[] = [];
+    for (
+      let tid: IonType | null = this.reader.next();
+      tid === IonTypes.STRUCT;
+      tid = this.reader.next()
+    ) {
+      this.reader.stepIn();
+      let tempEvent: IonEvent = this.marshalEvent();
+      if (tempEvent.eventType === IonEventType.CONTAINER_START) {
+        currentContainer.push(tempEvent);
+        this.events.push(tempEvent);
+        currentContainerIndex.push(this.events.length);
+      } else if (tempEvent.eventType === IonEventType.CONTAINER_END) {
+        this.endContainer(
+          currentContainer.pop()!,
+          currentContainerIndex.pop()!
+        );
+      } else if (
+        tempEvent.eventType === IonEventType.SCALAR ||
+        tempEvent.eventType === IonEventType.STREAM_END
+      ) {
+        this.events.push(tempEvent);
+      } else {
+        throw new Error("Unexpected eventType: " + tempEvent.eventType);
+      }
+      this.reader.stepOut();
+    }
+  }
+
+  private marshalEvent(): IonEvent {
+    let currentEvent = {};
+    for (let tid: IonType | null; (tid = this.reader.next()); ) {
+      let fieldName = this.reader.fieldName();
+      if (fieldName && currentEvent[fieldName] !== undefined)
+        throw new Error("Repeated event field: " + fieldName);
+      switch (fieldName) {
+        case "event_type": {
+          currentEvent[fieldName] = this.reader.stringValue();
+          break;
+        }
+
+        case "ion_type": {
+          currentEvent[fieldName] = this.parseIonType();
+          break;
+        }
+
+        case "field_name": {
+          currentEvent[
+            fieldName
+          ] = this.resolveFieldNameFromSerializedSymbolToken();
+          break;
+        }
+
+        case "annotations": {
+          currentEvent[fieldName] = this.parseAnnotations();
+          break;
+        }
+
+        case "value_text": {
+          let tempString: string = this.reader.stringValue()!;
+          if (tempString.substr(0, 5) === "$ion_")
+            tempString = "$ion_user_value::" + tempString;
+          let tempReader: Reader = makeReader(tempString);
+          tempReader.next();
+          let tempValue = tempReader.value();
+          currentEvent["isNull"] = tempReader.isNull();
+          currentEvent[fieldName] = tempValue;
+          break;
+        }
+
+        case "value_binary": {
+          currentEvent[fieldName] = this.parseBinaryValue();
+          break;
+        }
+
+        case "imports": {
+          currentEvent[fieldName] = this.parseImports();
+          break;
+        }
+
+        case "depth": {
+          currentEvent[fieldName] = this.reader.numberValue();
+          break;
+        }
+
+        default:
+          throw new Error("Unexpected event field: " + fieldName);
+      }
+    }
+    let eventType: IonEventType;
+    switch (currentEvent["event_type"]) {
+      case "CONTAINER_START":
+        eventType = IonEventType.CONTAINER_START;
+        break;
+      case "STREAM_END":
+        eventType = IonEventType.STREAM_END;
+        break;
+      case "CONTAINER_END":
+        eventType = IonEventType.CONTAINER_END;
+        break;
+      case "SCALAR":
+        eventType = IonEventType.SCALAR;
+        break;
+      case "SYMBOL_TABLE":
+        throw new Error("Symbol tables unsupported");
+    }
+    let fieldname =
+      currentEvent["field_name"] !== undefined
+        ? currentEvent["field_name"]
+        : null;
+    if (!currentEvent["annotations"]) currentEvent["annotations"] = [];
+
+    let textEvent = this.eventFactory.makeEvent(
+      eventType!,
+      currentEvent["ion_type"],
+      fieldname,
+      currentEvent["depth"],
+      currentEvent["annotations"],
+      currentEvent["isNull"],
+      currentEvent["value_text"]
+    );
+
+    if (eventType! === IonEventType.SCALAR) {
+      let binaryEvent = this.eventFactory.makeEvent(
+        eventType,
+        currentEvent["ion_type"],
+        fieldname,
+        currentEvent["depth"],
+        currentEvent["annotations"],
+        currentEvent["isNull"],
+        currentEvent["value_binary"]
+      );
+      if (!textEvent.equals(binaryEvent)) {
+        throw new Error(
+          `Text event ${currentEvent["value_text"]} does not equal binary event ${currentEvent["value_binary"]}`
+        );
+      }
+    }
+    return textEvent;
+  }
+
+  private parseIonType(): IonType {
+    let input: string = this.reader.stringValue()!.toLowerCase();
+    switch (input) {
+      case "null": {
+        return IonTypes.NULL;
+      }
+      case "bool": {
+        return IonTypes.BOOL;
+      }
+      case "int": {
+        return IonTypes.INT;
+      }
+      case "float": {
+        return IonTypes.FLOAT;
+      }
+      case "decimal": {
+        return IonTypes.DECIMAL;
+      }
+      case "timestamp": {
+        return IonTypes.TIMESTAMP;
+      }
+      case "symbol": {
+        return IonTypes.SYMBOL;
+      }
+      case "string": {
+        return IonTypes.STRING;
+      }
+      case "clob": {
+        return IonTypes.CLOB;
+      }
+      case "blob": {
+        return IonTypes.BLOB;
+      }
+      case "list": {
+        return IonTypes.LIST;
+      }
+      case "sexp": {
+        return IonTypes.SEXP;
+      }
+      case "struct": {
+        return IonTypes.STRUCT;
+      }
+      default: {
+        throw new Error("i: " + input);
+      }
+    }
+  }
+
+  private parseAnnotations(): string[] {
+    let annotations: string[] = [];
+    if (this.reader.isNull()) {
+      return annotations;
+    } else {
+      this.reader.stepIn();
+      for (let tid; (tid = this.reader.next()); ) {
+        if (tid == IonTypes.STRUCT) {
+          this.reader.stepIn();
+          let type = this.reader.next();
+          if (this.reader.fieldName() == "text" && type == IonTypes.STRING) {
+            let text = this.reader.stringValue();
+            if (text !== null) {
+              annotations.push(text!);
+            }
+          } else if (
+            this.reader.fieldName() == "importLocation" &&
+            type == IonTypes.INT
+          ) {
+            let symtab = defaultLocalSymbolTable();
+            let symbol = symtab.getSymbolText(this.reader.numberValue()!);
+            if (symbol === undefined || symbol === null) {
+              throw new Error(
+                "Unresolvable symbol ID, symboltokens unsupported."
+              );
+            }
+            annotations.push(symbol);
+          }
+          this.reader.stepOut();
+        }
+      }
+      this.reader.stepOut();
+      return annotations;
+    }
+  }
+
+  private parseBinaryValue(): any {
+    //convert list of ints to array of bytes and pass the currentBuffer to a binary reader, generate value from factory.
+    //start with a null check
+    if (this.reader.isNull()) return null;
+    let numBuffer: number[] = [];
+    this.reader.stepIn();
+    let tid: IonType | null = this.reader.next();
+    while (tid) {
+      numBuffer.push(this.reader.numberValue()!);
+      tid = this.reader.next();
+    }
+    this.reader.stepOut();
+    const bufArray = new Uint8Array(numBuffer as ReaderOctetBuffer);
+    let tempReader: Reader = new BinaryReader(new BinarySpan(bufArray));
+    tempReader.next();
+    return tempReader.value();
+  }
+
+  private parseImports(): any {
+    //TODO needed for symboltoken support.
+    return this.reader.value();
+  }
+
+  /** Parse the field name (Symbol Token) into text/symbol
+   *  example: {event_type: SCALAR, ion_type: INT, field_name: {text:"foo"}, value_text: "1", value_binary: [0x21, 0x01], depth:1}
+   *  for more information: https://github.com/amzn/ion-test-driver#symboltoken-1
+   */
+  private resolveFieldNameFromSerializedSymbolToken(): string | null {
+    if (this.reader.isNull()) {
+      return null;
+    }
+    this.reader.stepIn();
+    let type = this.reader.next();
+    if (this.reader.fieldName() == "text" && type == IonTypes.STRING) {
+      let text = this.reader.stringValue();
+      if (text !== null) {
+        this.reader.stepOut();
+        return text;
+      }
+    } else if (
+      this.reader.fieldName() == "importLocation" &&
+      type == IonTypes.INT
+    ) {
+      let symtab = defaultLocalSymbolTable();
+      let symbol = symtab.getSymbolText(this.reader.numberValue()!);
+      if (symbol === undefined || symbol === null) {
+        throw new Error("Unresolvable symbol ID, symboltokens unsupported.");
+      }
+      this.reader.stepOut();
+      return symbol;
+    }
+    return null;
+  }
+}


Test patch:
diff --git a/test-driver/package.json b/test-driver/package.json
new file mode 100644
index 0000000..064cf3b
--- /dev/null
+++ b/test-driver/package.json
@@ -0,0 +1,23 @@
+{
+  "name": "test-driver",
+  "version": "1.0.0",
+  "description": "A JavaScript implementation of ion-test-driver",
+  "main": "dist/Cli.js",
+  "scripts": {
+    "build": "grunt build:test-driver"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/amzn/ion-js.git"
+  },
+  "author": "The Ion Team <ion-team@amazon.com> (https://amzn.github.io/ion-docs/index.html)",
+  "license": "Apache-2.0",
+  "bugs": {
+    "url": "https://github.com/amzn/ion-js/issues"
+  },
+  "homepage": "https://github.com/amzn/ion-js#readme",
+  "dependencies": {
+    "yargs": "^15.4.1",
+    "ion-js": "../"
+  }
+}
diff --git a/test-driver/src/Cli.ts b/test-driver/src/Cli.ts
new file mode 100644
index 0000000..08dab1f
--- /dev/null
+++ b/test-driver/src/Cli.ts
@@ -0,0 +1,55 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import yargs from "yargs";
+
+/**
+ * This will cause `yargs` to look in other .ts/.js files in the same directory for command modules.
+ *
+ * For more information, see:
+ * Command modules: https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
+ * commandDir: https://github.com/yargs/yargs/blob/master/docs/advanced.md#commanddirdirectory-opts
+ */
+yargs
+    .commandDir(__dirname,{
+        extensions: ['ts','js'],
+    })
+    .options({
+        'output': {
+            alias: 'o',
+            description: 'Output location. [default: stdout]',
+        },
+        'output-format': {
+            alias: 'f',
+            choices: ['pretty', 'text', 'binary', 'events', 'none'] as const,
+            default: 'pretty',
+            description: "Output format, from the set (text | pretty | binary |\n"
+                + "events | none). 'events' is only available with the\n"
+                + "'process' command, and outputs a serialized EventStream\n"
+                + "representing the input Ion stream(s)."
+        },
+        'error-report': {
+            alias: 'e',
+            description: 'ErrorReport location. [default: stderr]',
+        }
+    }).argv;
+
+
+
+
+
+
+
+
diff --git a/test-driver/src/CliCommonArgs.ts b/test-driver/src/CliCommonArgs.ts
new file mode 100644
index 0000000..d983ca5
--- /dev/null
+++ b/test-driver/src/CliCommonArgs.ts
@@ -0,0 +1,55 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import {OutputFormat} from "./OutputFormat";
+import fs from "fs";
+import yargs from "yargs";
+
+/** common CLI arguments structure
+ *  for more information: https://github.com/amzn/ion-test-driver#standardized-cli
+ */
+export class IonCliCommonArgs {
+    inputFiles: Array<string>;
+    outputFormatName: OutputFormat;
+
+    // outputFile,errorReportFile: 'any' datatype to use stdout as well as file write stream to write ion data
+    // for more information : https://github.com/amzn/ion-js/issues/627
+    outputFile: any;
+    errorReportFile: any;
+
+    constructor(argv: yargs.Arguments) {
+        this.outputFile = argv["output"] ? fs.createWriteStream(argv["output"] as string, {flags: 'w'}) : process.stdout;
+        // create error output stream (DEFAULT: stderr)
+        this.errorReportFile = argv["error-report"] ? fs.createWriteStream(argv["error-report"] as string, {flags: 'w'}) : process.stderr;
+        this.outputFormatName = argv["output-format"] as OutputFormat;
+        this.inputFiles = argv["input-file"] as Array<string>;
+    }
+
+    getOutputFile(): any {
+        return this.outputFile;
+    }
+
+    getErrorReportFile(): any {
+        return this.errorReportFile;
+    }
+
+    getInputFiles(): Array<string> {
+        return this.inputFiles;
+    }
+
+    getOutputFormatName(): OutputFormat {
+        return this.outputFormatName;
+    }
+}
\ No newline at end of file
diff --git a/test-driver/src/CliCompareArgs.ts b/test-driver/src/CliCompareArgs.ts
new file mode 100644
index 0000000..de6bbcb
--- /dev/null
+++ b/test-driver/src/CliCompareArgs.ts
@@ -0,0 +1,31 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import {ComparisonType} from "./Compare";
+import {IonCliCommonArgs} from "./CliCommonArgs";
+
+/** CLI arguments for compare structure */
+export class IonCompareArgs extends IonCliCommonArgs{
+    comparisonType : ComparisonType;
+
+    constructor(props) {
+        super(props);
+        this.comparisonType = props["comparison-type"] as ComparisonType;
+    }
+
+    getComparisonType(): ComparisonType {
+        return this.comparisonType;
+    }
+}
\ No newline at end of file
diff --git a/test-driver/src/CliError.ts b/test-driver/src/CliError.ts
new file mode 100644
index 0000000..e7db045
--- /dev/null
+++ b/test-driver/src/CliError.ts
@@ -0,0 +1,58 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import {IonTypes, makeTextWriter} from "ion-js";
+
+/** Error Types symbol[READ | WRITE | STATE] */
+export enum ErrorType {
+    READ = "READ",
+    WRITE = "WRITE",
+    STATE = "STATE"
+}
+
+/** Error structure for error report
+ * for more information: https://github.com/amzn/ion-test-driver#errorreport
+ */
+export class IonCliError {
+    errorType: ErrorType;
+    eventIndex: number;
+    location: string;
+    message: string;
+    errorReportFile: any;
+
+    constructor(errorType: ErrorType, location: string, message: string, errorReportFile: any, eventIndex: number = 0) {
+        this.errorType = errorType;
+        this.location = location;
+        this.message = message;
+        this.errorReportFile = errorReportFile;
+        this.eventIndex = eventIndex;
+    }
+
+    writeErrorReport() {
+        let writer = makeTextWriter();
+        writer.stepIn(IonTypes.STRUCT);
+        writer.writeFieldName('error_type');
+        writer.writeSymbol(this.errorType);
+        writer.writeFieldName('message');
+        writer.writeString(this.message);
+        writer.writeFieldName('location');
+        writer.writeString(this.location);
+        writer.writeFieldName('event_index');
+        writer.writeInt(this.eventIndex);
+        writer.stepOut();
+        this.errorReportFile.write(writer.getBytes());
+        this.errorReportFile.write("\n");
+    }
+}
\ No newline at end of file
diff --git a/test-driver/src/Compare.ts b/test-driver/src/Compare.ts
new file mode 100644
index 0000000..6973979
--- /dev/null
+++ b/test-driver/src/Compare.ts
@@ -0,0 +1,228 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import {OutputFormat} from './OutputFormat';
+import {Writer} from "ion-js";
+import {IonComparisonReport} from "./ComparisonReport";
+import {ComparisonContext} from "./ComparisonContext";
+import {IonCompareArgs} from "./CliCompareArgs";
+import {ComparisonResult, ComparisonResultType} from "ion-js";
+import {IonEvent, IonEventType} from "ion-js";
+import {IonTypes} from "ion-js";
+import {makeReader} from "ion-js";
+import {IonEventStream} from "ion-js";
+
+/**
+ * The `command`, `describe`, and `handler` exports below are part of the yargs command module API
+ * See: https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
+ */
+export const command = 'compare <input-file..>'
+
+export const describe = "Compare all inputs (which may contain Ion streams and/or EventStreams) against all other inputs " +
+    "using the Ion data model's definition of equality. Write a ComparisonReport to the output.";
+
+export const builder = {
+    'comparison-type': {
+        alias: "y",
+        default: 'basic',
+        choices: ['basic', 'equivs', 'non-equivs', 'equiv-timeline'],
+        describe: "Comparison semantics to be used with the compare command, from the set (basic | equivs | non-equivs |" +
+            "equiv-timeline). Any embedded streams in the inputs are compared for EventStream equality. 'basic' performs" +
+            "a standard data-model comparison between the corresponding events (or embedded streams) in the inputs." +
+            "'equivs' verifies that each value (or embedded stream) in a top-level sequence is equivalent to every other" +
+            "value (or embedded stream) in that sequence. 'non-equivs' does the same, but verifies that the values (or" +
+            "embedded streams) are not equivalent. 'equiv-timeline' is the same as 'equivs', except that when top-level" +
+            "sequences contain timestamp values, they are considered equivalent if they represent the same instant" +
+            "regardless of whether they are considered equivalent by the Ion data model. [default: basic]"
+    }
+}
+
+export const handler = function (argv) {
+    let args = new IonCompareArgs(argv);
+    new Compare(args);
+}
+
+/** Comparison semantics to be used with compare command */
+export enum ComparisonType {
+    BASIC = "basic",
+    EQUIVS = "equivs",
+    NON_EQUIVS ="non-equivs",
+    EQUIV_TIMELINE = "equiv-timeline"
+}
+
+/**
+ * Compare all inputs (which may contain Ion streams and/or EventStreams) against all other inputs
+ * using the Ion data model's definition of equality. Write a ComparisonReport to the output.
+**/
+export class Compare {
+
+    constructor(parsedArgs: IonCompareArgs) {
+        let output_writer = OutputFormat.createIonWriter(parsedArgs.getOutputFormatName());
+        if (output_writer) {
+            this.compareFiles(output_writer, parsedArgs);
+        }
+    }
+
+    // compares files according to comparison type
+    compareFiles(ionOutputWriter: Writer, args: IonCompareArgs): void {
+        for (let pathFirst of args.getInputFiles()) {
+            for (let pathSecond of args.getInputFiles()) {
+                let comparisonType = args.getComparisonType();
+                if (comparisonType == ComparisonType.BASIC && pathFirst === pathSecond) {
+                    continue;
+                }
+                this.compareFilePair(ionOutputWriter, pathFirst, pathSecond, args);
+            }
+        }
+    }
+
+    private compareFilePair(ionOutputWriter: Writer, pathFirst: string, pathSecond: string, args: IonCompareArgs): void {
+        let lhs = new ComparisonContext(pathFirst, args);
+        let rhs = new ComparisonContext(pathSecond, args);
+        ionOutputWriter.close();
+        let comparisonType = args.getComparisonType();
+        let comparisonReport = new IonComparisonReport(lhs, rhs, args.getOutputFile(), comparisonType);
+        let result: ComparisonResult = new ComparisonResult();
+        let lhsEventStream = lhs.getEventStream();
+        let rhsEventStream = rhs.getEventStream();
+        if(comparisonType == ComparisonType.BASIC) {
+            if(lhsEventStream && rhsEventStream) {
+                result = lhs.getEventStream().compare(rhs.getEventStream());
+                if(result.result == ComparisonResultType.NOT_EQUAL) {
+                    comparisonReport.writeComparisonReport(result.result, result.message, result.actualIndex, result.expectedIndex);
+                }
+            }
+        }
+        else if(comparisonType == ComparisonType.EQUIVS || comparisonType == ComparisonType.EQUIV_TIMELINE
+            || comparisonType == ComparisonType.NON_EQUIVS) {
+            if(lhsEventStream && rhsEventStream) {
+                result = this.compareEquivs(lhs.getEventStream(), rhs.getEventStream(), comparisonType, comparisonReport);
+                if(comparisonType == ComparisonType.NON_EQUIVS && result.result == ComparisonResultType.EQUAL) {
+                    comparisonReport.writeComparisonReport(result.result, result.message, result.actualIndex, result.expectedIndex);
+                }
+                else if((comparisonType == ComparisonType.EQUIVS || comparisonType == ComparisonType.EQUIV_TIMELINE) && result.result == ComparisonResultType.NOT_EQUAL) {
+                    comparisonReport.writeComparisonReport(result.result, result.message, result.actualIndex, result.expectedIndex);
+                }
+            }
+        }
+
+    }
+
+    /**
+     *  equivs, non-equivs & equiv-timeline comparison of eventstreams
+     *
+     *  @param comparisonReport: optional argument to write a comparison report for the equivalence result
+     */
+    compareEquivs(actual: IonEventStream, expected: IonEventStream , comparisonType: ComparisonType, comparisonReport?: IonComparisonReport): ComparisonResult {
+        let actualIndex: number = 0;
+        let expectedIndex: number = 0;
+
+        while (actualIndex < actual.getEvents().length && expectedIndex < expected.getEvents().length) {
+            let actualEvent = actual.getEvents()[actualIndex];
+            let expectedEvent = expected.getEvents()[expectedIndex];
+
+            if(actualEvent.eventType == IonEventType.STREAM_END && expectedEvent.eventType == IonEventType.STREAM_END) {
+                break;
+            } else if(actualEvent.eventType == IonEventType.STREAM_END || expectedEvent.eventType == IonEventType.STREAM_END) {
+                throw new Error("Different number of comparison sets.");
+            } else if (!(actualEvent.ionType == IonTypes.LIST || actualEvent.ionType == IonTypes.SEXP)
+                || !(expectedEvent.ionType == IonTypes.LIST || expectedEvent.ionType == IonTypes.SEXP)) {
+                throw new Error("Comparison sets must be lists or s-expressions.");
+            } else if(actual.isEmbedded(actualEvent) as any ^ expected.isEmbedded(expectedEvent) as any) {
+                throw new Error("Both streams should be embedded streams.");
+            }
+
+            // both containers has type any as it maybe IonEventStream[] or IonEvent[]
+            // depending on the type compare method will be called
+            let actualContainer: any = [];
+            let expectedContainer: any = [];
+            if (actual.isEmbedded(actualEvent) && expected.isEmbedded(expectedEvent)) {
+                //we found a list of strings that we need to interpret as top level ion text streams.
+                actualContainer = this.parseEmbeddedStream(actualEvent);
+                expectedContainer = this.parseEmbeddedStream(expectedEvent);
+            } else {//we're in an sexp/list
+                actualContainer = this.parseContainer(actualEvent);
+                expectedContainer = this.parseContainer(expectedEvent);
+            }
+
+            for (let i = 0; i < actualContainer.length; i++) {
+                for (let j = 0; j < expectedContainer.length; j++) {
+                    // for non-equivs: not comparing same index's value as it will always be same.
+                    if (comparisonType == ComparisonType.NON_EQUIVS && i == j)
+                        continue;
+                    let actualContainerEvent: IonEvent = actualContainer[i];
+                    let expectedContainerEvent: IonEvent = expectedContainer[j];
+                    let eventResult;
+
+                    if (comparisonType == ComparisonType.EQUIV_TIMELINE && actualContainerEvent.ionType == IonTypes.TIMESTAMP) {
+                        let ionTimestampActual = actualContainerEvent.ionValue;
+                        let ionTimestampExpected = expectedContainerEvent.ionValue;
+                        eventResult = ionTimestampActual.compareTo(ionTimestampExpected) == 0 ? new ComparisonResult(ComparisonResultType.EQUAL)
+                            : new ComparisonResult(ComparisonResultType.NOT_EQUAL, ionTimestampActual + " vs. " + ionTimestampExpected);
+                    } else {
+                        eventResult = actualContainerEvent.compare(expectedContainerEvent);
+                    }
+
+                    if ((comparisonType == ComparisonType.EQUIVS || comparisonType == ComparisonType.EQUIV_TIMELINE)
+                        && eventResult.result == ComparisonResultType.NOT_EQUAL) {
+                        if(comparisonReport) {
+                            // set event-index as the index of container + (i/j) representing the event-index inside container
+                            // + 1 to skip the first event (the container event)
+                            comparisonReport.writeComparisonReport(ComparisonResultType.NOT_EQUAL, eventResult.message, actualIndex+ i + 1, expectedIndex + j + 1);
+                        }
+                        return new ComparisonResult(ComparisonResultType.NOT_EQUAL);
+                    } else if (comparisonType == ComparisonType.NON_EQUIVS && eventResult.result == ComparisonResultType.EQUAL) {
+                        if(comparisonReport) {
+                            // set event-index as the index of container + (i/j) representing the event-index inside container
+                            // + 1 to skip the first event (the container event)
+                            comparisonReport.writeComparisonReport(ComparisonResultType.EQUAL,
+                                "Both values are equal in non-equivs comparison.", actualIndex + i + 1, expectedIndex + j + 1);
+                        }
+                        return new ComparisonResult(ComparisonResultType.EQUAL);
+                    }
+                }
+            }
+
+            // set indices to the next container/event stream
+            actualIndex = actualIndex + actualEvent.ionValue.length + 1;
+            expectedIndex = expectedIndex + expectedEvent.ionValue.length + 1;
+        }
+        return new ComparisonResult(comparisonType == ComparisonType.NON_EQUIVS ? ComparisonResultType.NOT_EQUAL : ComparisonResultType.EQUAL);
+    }
+
+
+    // parse container into events
+    private parseContainer(event: IonEvent): IonEvent[] {
+        let container: IonEvent[] = [];
+        for (let j = 0; j < event.ionValue.length - 1; j++) {
+            container.push(event.ionValue[j]);
+            if (event.ionValue[j].eventType === IonEventType.CONTAINER_START) {
+                j += event.ionValue[j].ionValue.length;
+            }
+        }
+        return container;
+    }
+
+    // parse embedded stream into events
+    private parseEmbeddedStream(event: IonEvent): any {
+        let container: any = [];
+        for (let j = 0; j < event.ionValue.length - 1; j++) {
+            if(event.ionValue[j].eventType == IonEventType.STREAM_END)
+                continue;
+            container.push(new IonEventStream(makeReader(event.ionValue[j].ionValue)));
+        }
+        return container;
+    }
+}
\ No newline at end of file
diff --git a/test-driver/src/ComparisonContext.ts b/test-driver/src/ComparisonContext.ts
new file mode 100644
index 0000000..b22a2c9
--- /dev/null
+++ b/test-driver/src/ComparisonContext.ts
@@ -0,0 +1,122 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import {IonEventStream} from "ion-js";
+import {Reader} from "ion-js";
+import {IonTypes, makeReader, Writer} from "ion-js";
+import fs from "fs";
+import {IonCompareArgs} from "./CliCompareArgs";
+import {ErrorType, IonCliError} from "./CliError";
+import {IonEvent, IonEventFactory, IonEventType} from "ion-js";
+
+/**
+ * ComparisonContext to create a structure for lhs and rhs streams
+ * for more information: https://github.com/amzn/ion-test-driver#comparisonreport
+ */
+export class ComparisonContext {
+    location: string;
+    eventStream: IonEventStream;
+
+    constructor(path: string, args: IonCompareArgs){
+        this.location = path;
+        let ionReader = this.createReadersForComparison(args);
+        this.setEventStream(ionReader, path, args);
+    }
+
+    createReadersForComparison(args: IonCompareArgs): Reader {
+        let ionReader;
+        try {
+            ionReader = makeReader(this.getInput(this.getLocation()));
+        } catch (Error) {
+            new IonCliError(ErrorType.READ, this.getLocation(), Error.message, args.getErrorReportFile()).writeErrorReport();
+        }
+        return ionReader;
+    }
+
+    getInput(path: string): string | Buffer {
+        let options = path.endsWith(".10n") ? null : "utf8";
+        return fs.readFileSync(path, options);
+    }
+
+    setEventStream(ionReader: Reader, path: string, args: IonCompareArgs) {
+        let events: IonEvent[] = [];
+        try{
+            let eventStream = new IonEventStream(ionReader);
+
+            if(!eventStream.isEventStream) {
+                // processes input stream(text or binary) into event stream
+                events = this.collectInputStreamEvents(events, eventStream);
+                eventStream.events = events;
+            }
+            this.eventStream = eventStream;
+        } catch (EventStreamError) {
+            if(EventStreamError.eventstream) {
+                events.push(...EventStreamError.eventstream);
+            }
+            if(EventStreamError.type === "READ") {
+                new IonCliError(ErrorType.READ, path, EventStreamError.message, args.getErrorReportFile(), events.length).writeErrorReport();
+            }
+            else if(EventStreamError.type === "WRITE") {
+                new IonCliError(ErrorType.WRITE, path, EventStreamError.message, args.getErrorReportFile()).writeErrorReport();
+            }
+            else {
+                new IonCliError(ErrorType.STATE, path, EventStreamError.message, args.getErrorReportFile()).writeErrorReport();
+            }
+        }
+    }
+
+    collectInputStreamEvents(events: IonEvent[], eventStream: IonEventStream): IonEvent[] {
+        for(let i = 0; i < eventStream.events.length; i++) {
+            let event = eventStream.events[i];
+            events.push(event);
+            if (eventStream.isEmbedded(event)) {
+                let tempEvents: IonEvent[] = [];
+                for (let j = 0; j < event.ionValue.length - 1; j++) {
+                    tempEvents.push(...new IonEventStream(makeReader(event.ionValue[j].ionValue)).getEvents());
+                }
+                i = i + (event.ionValue.length - 1);
+                let eventFactory = new IonEventFactory();
+                tempEvents.push(eventFactory.makeEvent(IonEventType.CONTAINER_END, event.ionType!, null, event.depth, [], false, null));
+                event.ionValue = tempEvents;
+                events.push(...tempEvents.slice(0,-1));
+            }
+        }
+        return events;
+    }
+
+    getEventStream() {
+        return this.eventStream;
+    }
+
+    getLocation(): string {
+        return this.location;
+    }
+
+    writeComparisonContext(ionOutputWriter: Writer, field_name: string, event_index: number) {
+        ionOutputWriter.writeFieldName(field_name);
+        ionOutputWriter.stepIn(IonTypes.STRUCT);
+        ionOutputWriter.writeFieldName("location");
+        ionOutputWriter.writeString(this.location);
+        ionOutputWriter.writeFieldName("event");
+        if(this.getEventStream().getEvents().length > 0) {
+            this.getEventStream().getEvents()[event_index].write(ionOutputWriter);
+        } else {
+            ionOutputWriter.writeNull(IonTypes.NULL);
+        }
+        ionOutputWriter.writeFieldName("event_index");
+        ionOutputWriter.writeInt(event_index);
+        ionOutputWriter.stepOut();
+    }
+}
\ No newline at end of file
diff --git a/test-driver/src/ComparisonReport.ts b/test-driver/src/ComparisonReport.ts
new file mode 100644
index 0000000..3752446
--- /dev/null
+++ b/test-driver/src/ComparisonReport.ts
@@ -0,0 +1,50 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import {ComparisonType} from "./Compare";
+import {IonTypes, makeTextWriter} from "ion-js";
+import {ComparisonContext} from "./ComparisonContext";
+import {ComparisonResultType} from "ion-js";
+
+/** comparison report structure for compare
+ *  for more information: https://github.com/amzn/ion-test-driver#comparisonreport
+ */
+export class IonComparisonReport {
+    lhs: ComparisonContext;
+    rhs: ComparisonContext;
+    comparisonReportFile: any;
+    comparisonType: ComparisonType;
+
+    constructor(lhs: ComparisonContext, rhs: ComparisonContext, comparisonReportFile: any, comparisonType: ComparisonType) {
+        this.lhs = lhs;
+        this.rhs = rhs;
+        this.comparisonReportFile = comparisonReportFile;
+        this.comparisonType = comparisonType;
+    }
+
+    writeComparisonReport(result: ComparisonResultType, message: string, event_index_lhs: number, event_index_rhs: number) {
+        let writer = makeTextWriter();
+        writer.stepIn(IonTypes.STRUCT);
+        writer.writeFieldName('result');
+        writer.writeSymbol(result);
+        this.lhs.writeComparisonContext(writer, "lhs", event_index_lhs);
+        this.rhs.writeComparisonContext(writer, "rhs", event_index_rhs);
+        writer.writeFieldName('message');
+        writer.writeString(message);
+        writer.stepOut();
+        this.comparisonReportFile.write(writer.getBytes());
+        this.comparisonReportFile.write("\n");
+    }
+}
\ No newline at end of file
diff --git a/test-driver/src/OutputFormat.ts b/test-driver/src/OutputFormat.ts
new file mode 100644
index 0000000..633f9d6
--- /dev/null
+++ b/test-driver/src/OutputFormat.ts
@@ -0,0 +1,49 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import {Writer} from 'ion-js';
+import {
+    makePrettyWriter,
+    makeBinaryWriter,
+    makeTextWriter
+} from 'ion-js';
+
+export enum OutputFormat {
+    PRETTY = "pretty",
+    TEXT = "text",
+    BINARY = "binary",
+    EVENTS = "events",
+    NONE = "none"
+}
+
+/** gets the writer corresponding to the output format **/
+export namespace OutputFormat {
+    export function createIonWriter(name: OutputFormat) : Writer | null {
+        switch (name) {
+            case OutputFormat.PRETTY:
+                return makePrettyWriter();
+            case OutputFormat.TEXT:
+                return makeTextWriter();
+            case OutputFormat.BINARY:
+                return makeBinaryWriter();
+            case OutputFormat.EVENTS:
+                return makePrettyWriter();
+            case OutputFormat.NONE:
+                return null;
+            default:
+                throw new Error("Output Format " + name + " unexpected.");
+        }
+    }
+}
\ No newline at end of file
diff --git a/test-driver/src/Process.ts b/test-driver/src/Process.ts
new file mode 100644
index 0000000..8ed8380
--- /dev/null
+++ b/test-driver/src/Process.ts
@@ -0,0 +1,193 @@
+/*!
+ * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import fs from 'fs';
+import {OutputFormat} from './OutputFormat';
+import {IonEventStream} from "ion-js";
+import {Writer} from "ion-js";
+import {IonTypes, makeReader, Reader} from "ion-js";
+import {IonEvent} from "ion-js";
+import {ErrorType, IonCliError} from "./CliError";
+import {IonCliCommonArgs} from "./CliCommonArgs";
+
+/**
+ * The `command`, `describe`, and `handler` exports below are part of the yargs command module API
+ * See: https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
+ */
+export const command = 'process <input-file..>'
+
+export const describe = "Read the input file(s) (optionally, specifying ReadInstructions or a filter) and re-write in" +
+    "the format specified by --output.";
+
+export const handler = function (argv) {
+    let args = new IonCliCommonArgs(argv);
+    new Process(args);
+}
+
+/**
+ *  Read the input file(s) and re-write in the format specified by --output-format
+ *  Create error report through --error-report and output file specified by --output
+ */
+export class Process {
+    private EVENT_STREAM = "$ion_event_stream";
+
+    constructor(parsedArgs: IonCliCommonArgs) {
+        let output_writer = OutputFormat.createIonWriter(parsedArgs.getOutputFormatName());
+        if (output_writer) {
+            if (parsedArgs.getOutputFormatName() == OutputFormat.EVENTS) {
+                this.processEvents(output_writer, parsedArgs);
+            } else {
+                this.processFiles(output_writer, parsedArgs);
+            }
+        }
+    }
+
+    private getInput(path: string): string | Buffer {
+        let options = path.endsWith(".10n") ? null : "utf8";
+        return fs.readFileSync(path, options);
+    }
+
+    // create a reader for an input stream
+    createReaderForProcess(path: string, args: IonCliCommonArgs): Reader {
+        let ionReader;
+        try {
+            ionReader = makeReader(this.getInput(path));
+        } catch (Error) {
+            new IonCliError(ErrorType.READ, path, Error.message, args.getErrorReportFile()).writeErrorReport();
+        }
+        return ionReader;
+    }
+
+    private isEventStream(ionReader: Reader): boolean {
+        let type = ionReader.next();
+        return type != null
+            && type == IonTypes.SYMBOL
+            && ionReader.stringValue() == this.EVENT_STREAM;
+    }
+
+    // processes input streams into event streams
+    processEvents(ionOutputWriter: Writer, args: IonCliCommonArgs): void {
+        for (let path of args.getInputFiles()) {
+            this.processEvent(ionOutputWriter, args, path);
+        }
+        args.getOutputFile().write(ionOutputWriter.getBytes());
+    }
+
+    private processEvent(ionOutputWriter: Writer, args: IonCliCommonArgs, path: string): void {
+        let ionReader = this.createReaderForProcess(path, args);
+        this.writeToEventStream(ionOutputWriter, ionReader, path, args);
+    }
+
+    private writeToEventStream(ionOutputWriter: Writer, ionReader: Reader, path: string, args: IonCliCommonArgs): void {
+        let eventStream;
+        let writeEvents: IonEvent[] = [];
+        try {
+            eventStream = new IonEventStream(ionReader);
+
+            if(eventStream.isEventStream) {
+                //process event stream into event stream
+                writeEvents = eventStream.getEvents();
+            } else {
+                // processes input stream(text or binary) into event stream
+                writeEvents = this.collectInputStreamEvents(writeEvents, eventStream);
+            }
+        } catch (EventStreamError) {
+            if(EventStreamError.eventstream) {
+                writeEvents.push(...EventStreamError.eventstream);
+            }
+            if(EventStreamError.type === "READ") {
+                new IonCliError(ErrorType.READ, path, EventStreamError.message, args.getErrorReportFile(), writeEvents.length).writeErrorReport();
+            }
+            else if(EventStreamError.type === "WRITE") {
+                new IonCliError(ErrorType.WRITE, path, EventStreamError.message, args.getErrorReportFile()).writeErrorReport();
+            }
+            else {
+                new IonCliError(ErrorType.STATE, path, EventStreamError.message, args.getErrorReportFile()).writeErrorReport();
+            }
+        } finally {
+            this.addEventsToWriter(ionOutputWriter, writeEvents);
+        }
+    }
+
+    private addEventsToWriter(ionOutputWriter: Writer, writeEvents: IonEvent[]): void {
+        ionOutputWriter.writeSymbol(this.EVENT_STREAM);
+        for (let event of writeEvents) {
+            event.write(ionOutputWriter);
+        }
+        ionOutputWriter.close();
+    }
+
+    private collectInputStreamEvents(writeEvents: IonEvent[], eventStream: IonEventStream): IonEvent[] {
+        for(let i = 0; i < eventStream.getEvents().length; i++) {
+            let event = eventStream.getEvents()[i];
+            writeEvents.push(event);
+
+            if (eventStream.isEmbedded(event)) {
+                for (let j = 0; j < event.ionValue.length - 1; j++) {
+                    writeEvents.push(...new IonEventStream(makeReader(event.ionValue[j].ionValue)).getEvents());
+                }
+                i = i + (event.ionValue.length - 1);
+            }
+        }
+        return writeEvents;
+    }
+
+    // process input stream into specified output format (except events)
+    processFiles(ionOutputWriter: Writer, args: IonCliCommonArgs): void {
+        for (let path of args.getInputFiles()) {
+            this.processFile(ionOutputWriter, args, path);
+        }
+        args.getOutputFile().write(ionOutputWriter.getBytes());
+    }
+
+    private processFile(ionOutputWriter: Writer, args: IonCliCommonArgs, path: string): void {
+        let ionReader = this.createReaderForProcess(path, args);
+        if (this.isEventStream(ionReader)) {
+            let reader = this.createReaderForProcess(path, args);
+            this.writeToProcessFileFromEventStream(ionOutputWriter, reader, path, args);
+        } else {
+            this.writeToProcessFile(ionOutputWriter, ionReader, path, args);
+        }
+    }
+
+    // write event stream to specified output format (except events)
+    private writeToProcessFileFromEventStream(ionOutputWriter: Writer, ionReader: Reader, path: string, args: IonCliCommonArgs): void {
+        let eventStream;
+        try {
+            eventStream = new IonEventStream(ionReader);
+            eventStream.writeIon(ionOutputWriter);
+        } catch (EventStreamError) {
+            if(EventStreamError.type === "READ") {
+                new IonCliError(ErrorType.READ, path, EventStreamError.message, args.getErrorReportFile(), EventStreamError.index).writeErrorReport();
+            }
+            else if(EventStreamError.type === "WRITE") {
+                new IonCliError(ErrorType.WRITE, path, EventStreamError.message, args.getErrorReportFile()).writeErrorReport();
+            }
+            else {
+                new IonCliError(ErrorType.STATE, path, EventStreamError.message, args.getErrorReportFile()).writeErrorReport();
+            }
+        }
+    }
+
+    // write input stream to specified output format (except events)
+    private writeToProcessFile(ionOutputWriter: Writer, ionReader: Reader, path: string, args: IonCliCommonArgs): void {
+        try {
+            ionOutputWriter.writeValues(ionReader);
+            ionOutputWriter.close();
+        } catch (Error) {
+            new IonCliError(ErrorType.WRITE, path, Error.message, args.getErrorReportFile()).writeErrorReport();
+        }
+    }
+}
\ No newline at end of file
diff --git a/test-driver/tsconfig.json b/test-driver/tsconfig.json
new file mode 100644
index 0000000..f9cbcdf
--- /dev/null
+++ b/test-driver/tsconfig.json
@@ -0,0 +1,27 @@
+// Test-driver Typscript Compiler Configuration
+// Targets CommonJS Module system and ES6 feature set.
+{
+  "include": [
+    "src/**"
+  ],
+  "exclude": [
+    "**/*.d.ts"
+  ],
+  "compilerOptions": {
+    "target": "es6",
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "esModuleInterop": true,
+    "lib": ["es6", "es2017.object"],
+    "declaration": false,
+    "inlineSources": true,
+    "sourceMap": true,
+    "strict": false,
+    "strictNullChecks": true,
+    "rootDirs": [
+      "src"
+    ],
+    "outDir": "dist",
+    "experimentalDecorators": true
+  }
+}
\ No newline at end of file
diff --git a/test/iontests.ts b/test/iontests.ts
index 6c31302..99cf8ea 100644
--- a/test/iontests.ts
+++ b/test/iontests.ts
@@ -15,11 +15,11 @@
 
 import {assert} from 'chai';
 import * as ion from '../src/Ion';
+import {IonType, Reader, Writer} from '../src/Ion';
 import * as fs from 'fs';
 import * as path from 'path';
-import {IonEventStream} from '../src/IonEventStream';
-import {IonEventType} from '../src/IonEvent';
-import {IonType, Reader, Writer} from '../src/Ion';
+import {IonEventStream} from '../src/events/IonEventStream';
+import {IonEventType} from "../src/events/IonEvent";
 
 function findFiles(folder: string, files: string[] = []) {
     fs.readdirSync(folder).forEach(file => {
diff --git a/test/mocha.opts b/test/mocha.opts
index 623735d..a837852 100644
--- a/test/mocha.opts
+++ b/test/mocha.opts
@@ -1,6 +1,8 @@
 --require ts-node/register
 --ui mocha-typescript
 --require source-map-support/register
+#Increasing test timeout for node v14, refer To: https://github.com/amzn/ion-js/issues/581
+--timeout 3000
 
 test/*.ts
-test/dom/*.ts
\ No newline at end of file
+test/dom/*.ts