Run test cases
diff --git a/.github/workflows/compile_test.yml b/.github/workflows/compile_test.yml
index 1960db2..59ddc6b 100644
--- a/.github/workflows/compile_test.yml
+++ b/.github/workflows/compile_test.yml
@@ -14,6 +14,11 @@
     steps:
       - uses: actions/checkout@v1
 
+      - name: Update submodules
+        run: |
+          git submodule init
+          git submodule update
+
       - name: Setup python
         uses: actions/setup-python@v1
         with:
@@ -25,3 +30,6 @@
 
       - name: Compile project
         run: make
+
+      - name: Run tests
+        run: python run-tests.py
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..0174b67
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "third-party/json5-tests"]
+	path = third-party/json5-tests
+	url = https://github.com/json5/json5-tests.git
diff --git a/requirements.txt b/requirements.txt
index c5ef52b..c963038 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+colorama == 0.*, >= 0.4.3
 cython == 0.*, >= 0.29.14
 more_itertools == 8.*, >= 8.0.2
 sphinx == 2.*, >= 2.2.2
diff --git a/run-tests.py b/run-tests.py
new file mode 100755
index 0000000..32cf988
--- /dev/null
+++ b/run-tests.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+
+from argparse import ArgumentParser
+from logging import basicConfig, INFO, getLogger
+from pathlib import Path
+from subprocess import Popen
+
+from colorama import init, Fore
+from pyjson5 import decode_io
+
+
+argparser = ArgumentParser(description='Run JSON5 parser tests')
+argparser.add_argument('tests', nargs='?', type=Path, default=Path('third-party/json5-tests'))
+
+suffix_implies_success = {
+    '.json': True,
+    '.json5': True,
+    '.txt': False,
+}
+
+if __name__ == '__main__':
+    basicConfig(level=INFO)
+    logger = getLogger(__name__)
+
+    init()
+
+    good = 0
+    bad = 0
+    severe = 0
+
+    args = argparser.parse_args()
+    index = 0
+    for path in sorted(args.tests.glob('*/*.*')):
+        kind = path.suffix.split('.')[-1]
+        expect_success = suffix_implies_success.get(path.suffix)
+        if expect_success is None:
+            continue
+
+        index += 1
+        category = path.parent.name
+        name = path.stem
+        try:
+            p = Popen(('/usr/bin/env', 'python', 'transcode-to-json.py', str(path)))
+            outcome = p.wait(5)
+        except Exception:
+            logger.error('Error while testing: %s', path, exc_info=True)
+            errors += 1
+            continue
+
+        is_success = outcome == 0
+        is_failure = outcome == 1
+        is_severe = outcome not in (0, 1)
+        is_good = is_success if expect_success else is_failure
+
+        code = (
+            Fore.RED + '๐Ÿ˜ฑ' if is_severe else
+            Fore.CYAN + '๐Ÿ˜„' if is_good else
+            Fore.YELLOW + '๐Ÿ˜ '
+        )
+        print(
+            '#', index, ' ', code, ' '
+            'Category <', category, '> | '
+            'Test <', name, '> | '
+            'Data <', kind, '> | '
+            'Expected <', 'pass' if expect_success else 'FAIL', '> | '
+            'Actual <', 'pass' if is_success else 'FAIL', '>',
+            Fore.RESET,
+            sep='',
+        )
+        if is_severe:
+            severe += 1
+        elif is_good:
+            good += 1
+        else:
+            bad += 1
+
+    is_severe = severe > 0
+    is_good = bad == 0
+    code = (
+        Fore.RED + '๐Ÿ˜ฑ' if is_severe else
+        Fore.CYAN + '๐Ÿ˜„' if is_good else
+        Fore.YELLOW + '๐Ÿ˜ '
+    )
+    print()
+    print(
+        code, ' ',
+        good, ' × correct outcome | ',
+        bad, ' × wrong outcome | ',
+        severe, ' × severe errors',
+        Fore.RESET,
+        sep=''
+    )
+    raise SystemExit(2 if is_severe else 0 if is_good else 1)
diff --git a/third-party/json5-tests b/third-party/json5-tests
new file mode 160000
index 0000000..c9af328
--- /dev/null
+++ b/third-party/json5-tests
@@ -0,0 +1 @@
+Subproject commit c9af328e6d77286d78b77b520c4622d588b544c0
diff --git a/transcode-to-json.py b/transcode-to-json.py
new file mode 100755
index 0000000..bb674b4
--- /dev/null
+++ b/transcode-to-json.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+from argparse import ArgumentParser
+from logging import basicConfig, DEBUG, getLogger
+from pathlib import Path
+
+from pyjson5 import decode, encode
+
+
+argparser = ArgumentParser(description='Run JSON5 parser tests')
+argparser.add_argument('input', type=Path)
+argparser.add_argument('output', nargs='?', type=Path)
+
+if __name__ == '__main__':
+    basicConfig(level=DEBUG)
+    logger = getLogger(__name__)
+
+    args = argparser.parse_args()
+    try:
+        # open() does not work with Paths in Python 3.5
+        with open(str(args.input.resolve()), 'rt') as f:
+            data = f.read()
+    except Exception:
+        logger.error('Could not even read file: %s', args.input, exc_info=True)
+        raise SystemExit(-1)
+
+    try:
+        obj = decode(data)
+    except Exception:
+        logger.error('Could not parse content: %s', args.input)
+        raise SystemExit(1)
+
+    try:
+        data = encode(obj)
+    except Exception:
+        logger.error('Could open stringify content: %s', args.input, exc_info=True)
+        raise SystemExit(2)
+
+    if args.output is not None:
+        try:
+            # open() does not work with Paths in Python 3.5
+            with open(str(args.output.resolve()), 'wt') as f:
+                f.write(data)
+        except Exception:
+            logger.error('Could open output file: %s', args.output, exc_info=True)
+            raise SystemExit(-1)
+
+    raise SystemExit(0)
+