diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4803cc9..95b8d7e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,3 +14,8 @@ jobs:
       uses: adafruit/workflows-circuitpython-libs/build@main
       with:
         package-prefix: "asyncio"
+
+  test-v9-0-0-alpha5:
+    uses: ./.github/workflows/run-tests.yml
+    with:
+      cp-version: 9.0.0-alpha.5
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 0000000..f6cf38d
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,63 @@
+# SPDX-FileCopyrightText: 2014 MicroPython & CircuitPython contributors (https://github.com/adafruit/circuitpython/graphs/contributors)
+#
+# SPDX-License-Identifier: MIT
+
+name: Run tests
+
+on:
+  workflow_call:
+    inputs:
+      cp-version:
+        required: true
+        type: string
+
+jobs:
+  run:
+    runs-on: ubuntu-22.04
+    env:
+      CP_VERSION: ${{ inputs.cp-version }}
+    steps:
+    - name: Set up repository
+      uses: actions/checkout@v3
+      with:
+        submodules: false
+        fetch-depth: 1
+    - name: Set up circuitpython repository
+      uses: actions/checkout@v3
+      with:
+        ref: ${{ inputs.cp-version }}
+        repository: adafruit/circuitpython
+        path: ./circuitpython/
+        submodules: false
+        fetch-depth: 1
+        fetch-tags: true
+    - name: Set up python
+      uses: actions/setup-python@v4
+      with:
+        python-version: 3.8
+    - name: CircuitPython dependencies
+      id: cp-deps
+      run: python tools/ci_fetch_deps.py tests
+      shell: bash
+      working-directory: ./circuitpython/
+    - name: Fetch relevant submodules
+      id: submodules
+      run: python tools/ci_fetch_deps.py tests
+      working-directory: ./circuitpython
+    - name: Install python dependencies
+      run: pip install -r requirements-dev.txt
+      shell: bash
+      working-directory: ./circuitpython/
+    - name: Build mpy-cross
+      run: make -C mpy-cross -j2
+      working-directory: ./circuitpython/
+    - name: Build unix port
+      run: make -C ports/unix VARIANT=coverage BUILD=build-coverage PROG=micropython -j2
+      working-directory: ./circuitpython/
+    - name: Run tests
+      run: ./run_tests.py
+      working-directory: tests
+      env:
+        MICROPY_CPYTHON3: python3.8
+        MICROPY_MICROPYTHON: ../circuitpython/ports/unix/build-coverage/micropython
+        MICROPYPATH: ../:../circuitpython/frozen/Adafruit_CircuitPython_Ticks
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 0000000..eb826a6
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+*.out
diff --git a/tests/asyncio/asyncio_await_return.py b/tests/asyncio/asyncio_await_return.py
new file mode 100644
index 0000000..5885de6
--- /dev/null
+++ b/tests/asyncio/asyncio_await_return.py
@@ -0,0 +1,28 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test that tasks return their value correctly to the caller
+
+import asyncio
+
+
+async def example():
+    return 42
+
+
+async def main():
+    # Call function directly via an await
+    print(await example())
+
+    # Create a task and await on it
+    task = asyncio.create_task(example())
+    print(await task)
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_await_return.py.exp b/tests/asyncio/asyncio_await_return.py.exp
new file mode 100644
index 0000000..4c963ba
--- /dev/null
+++ b/tests/asyncio/asyncio_await_return.py.exp
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+42
+42
diff --git a/tests/asyncio/asyncio_basic.py b/tests/asyncio/asyncio_basic.py
new file mode 100644
index 0000000..ebd7882
--- /dev/null
+++ b/tests/asyncio/asyncio_basic.py
@@ -0,0 +1,50 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+
+import asyncio
+import time
+
+
+if hasattr(time, "ticks_ms"):
+    ticks = time.ticks_ms
+    ticks_diff = time.ticks_diff
+else:
+    ticks = lambda: int(time.time() * 1000)
+    ticks_diff = lambda t1, t0: t1 - t0
+
+
+async def delay_print(t, s):
+    await asyncio.sleep(t)
+    print(s)
+
+
+async def main():
+    print("start")
+
+    await asyncio.sleep(0.001)
+    print("after sleep")
+
+    t0 = ticks()
+    await delay_print(0.2, "short")
+    t1 = ticks()
+    await delay_print(0.4, "long")
+    t2 = ticks()
+    await delay_print(-1, "negative")
+    t3 = ticks()
+
+    print(
+        "took {} {} {}".format(
+            round(ticks_diff(t1, t0), -2),
+            round(ticks_diff(t2, t1), -2),
+            round(ticks_diff(t3, t2), -2),
+        )
+    )
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_basic.py.exp b/tests/asyncio/asyncio_basic.py.exp
new file mode 100644
index 0000000..1729c28
--- /dev/null
+++ b/tests/asyncio/asyncio_basic.py.exp
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+start
+after sleep
+short
+long
+negative
+took 200 400 0
diff --git a/tests/asyncio/asyncio_basic2.py b/tests/asyncio/asyncio_basic2.py
new file mode 100644
index 0000000..b0e2abe
--- /dev/null
+++ b/tests/asyncio/asyncio_basic2.py
@@ -0,0 +1,26 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+
+import asyncio
+
+
+async def forever():
+    print("forever start")
+    await asyncio.sleep(10)
+
+
+async def main():
+    print("main start")
+    asyncio.create_task(forever())
+    await asyncio.sleep(0.001)
+    print("main done")
+    return 42
+
+
+print(asyncio.run(main()))
diff --git a/tests/asyncio/asyncio_basic2.py.exp b/tests/asyncio/asyncio_basic2.py.exp
new file mode 100644
index 0000000..81abe7a
--- /dev/null
+++ b/tests/asyncio/asyncio_basic2.py.exp
@@ -0,0 +1,7 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+main start
+forever start
+main done
+42
diff --git a/tests/asyncio/asyncio_cancel_fair.py b/tests/asyncio/asyncio_cancel_fair.py
new file mode 100644
index 0000000..f1b66fd
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_fair.py
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test fairness of cancelling a task
+# That tasks which continuously cancel each other don't take over the scheduler
+import asyncio
+
+
+async def task(id, other):
+    for i in range(3):
+        try:
+            print("start", id)
+            await asyncio.sleep(0)
+            print("done", id)
+        except asyncio.CancelledError as er:
+            print("cancelled", id)
+        if other is not None:
+            print(id, "cancels", other)
+            tasks[other].cancel()
+
+
+async def main():
+    global tasks
+    tasks = [
+        asyncio.create_task(task(0, 1)),
+        asyncio.create_task(task(1, 0)),
+        asyncio.create_task(task(2, None)),
+    ]
+    await tasks[2]
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_cancel_fair.py.exp b/tests/asyncio/asyncio_cancel_fair.py.exp
new file mode 100644
index 0000000..2fbde21
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_fair.py.exp
@@ -0,0 +1,27 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+start 0
+start 1
+start 2
+done 0
+0 cancels 1
+start 0
+cancelled 1
+1 cancels 0
+start 1
+done 2
+start 2
+cancelled 0
+0 cancels 1
+start 0
+cancelled 1
+1 cancels 0
+start 1
+done 2
+start 2
+cancelled 0
+0 cancels 1
+cancelled 1
+1 cancels 0
+done 2
diff --git a/tests/asyncio/asyncio_cancel_fair2.py b/tests/asyncio/asyncio_cancel_fair2.py
new file mode 100644
index 0000000..6f61826
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_fair2.py
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test fairness of cancelling a task
+# That tasks which keeps being cancelled by multiple other tasks gets a chance to run
+import asyncio
+
+
+async def task_a():
+    try:
+        while True:
+            print("sleep a")
+            await asyncio.sleep(0)
+    except asyncio.CancelledError:
+        print("cancelled a")
+
+
+async def task_b(id, other):
+    while other.cancel():
+        print("sleep b", id)
+        await asyncio.sleep(0)
+    print("done b", id)
+
+
+async def main():
+    t = asyncio.create_task(task_a())
+    for i in range(3):
+        asyncio.create_task(task_b(i, t))
+    await t
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_cancel_fair2.py.exp b/tests/asyncio/asyncio_cancel_fair2.py.exp
new file mode 100644
index 0000000..1315b21
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_fair2.py.exp
@@ -0,0 +1,11 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+sleep a
+sleep b 0
+sleep b 1
+sleep b 2
+cancelled a
+done b 0
+done b 1
+done b 2
diff --git a/tests/asyncio/asyncio_cancel_self.py b/tests/asyncio/asyncio_cancel_self.py
new file mode 100644
index 0000000..25491d7
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_self.py
@@ -0,0 +1,32 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test a task cancelling itself (currently unsupported)
+import asyncio
+
+
+async def task():
+    print("task start")
+    global_task.cancel()
+
+
+async def main():
+    global global_task
+    global_task = asyncio.create_task(task())
+    try:
+        await global_task
+    except asyncio.CancelledError:
+        print("main cancel")
+    print("main done")
+
+
+try:
+    asyncio.run(main())
+except RuntimeError as er:
+    print(er)
diff --git a/tests/asyncio/asyncio_cancel_self.py.exp b/tests/asyncio/asyncio_cancel_self.py.exp
new file mode 100644
index 0000000..36d0023
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_self.py.exp
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+task start
+can't cancel self
diff --git a/tests/asyncio/asyncio_cancel_task.py b/tests/asyncio/asyncio_cancel_task.py
new file mode 100644
index 0000000..7962999
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_task.py
@@ -0,0 +1,91 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test cancelling a task
+
+try:
+    import asyncio
+except ImportError:
+    print("SKIP")
+    raise SystemExit
+
+
+async def task(s, allow_cancel):
+    try:
+        print("task start")
+        await asyncio.sleep(s)
+        print("task done")
+    except asyncio.CancelledError as er:
+        print("task cancel")
+        if allow_cancel:
+            raise er
+
+
+async def task2(allow_cancel):
+    print("task 2")
+    try:
+        await asyncio.create_task(task(0.05, allow_cancel))
+    except asyncio.CancelledError as er:
+        print("task 2 cancel")
+        raise er
+    print("task 2 done")
+
+
+async def main():
+    # Cancel task immediately
+    t = asyncio.create_task(task(2, True))
+    print(t.cancel())
+
+    # Cancel task after it has started
+    t = asyncio.create_task(task(2, True))
+    await asyncio.sleep(0.01)
+    print(t.cancel())
+    print("main sleep")
+    await asyncio.sleep(0.01)
+
+    # Cancel task multiple times after it has started
+    t = asyncio.create_task(task(2, True))
+    await asyncio.sleep(0.01)
+    for _ in range(4):
+        print(t.cancel())
+    print("main sleep")
+    await asyncio.sleep(0.01)
+
+    # Await on a cancelled task
+    print("main wait")
+    try:
+        await t
+    except asyncio.CancelledError:
+        print("main got CancelledError")
+
+    # Cancel task after it has finished
+    t = asyncio.create_task(task(0.01, False))
+    await asyncio.sleep(0.05)
+    print(t.cancel())
+
+    # Nested: task2 waits on task, task2 is cancelled (should cancel task then task2)
+    print("----")
+    t = asyncio.create_task(task2(True))
+    await asyncio.sleep(0.01)
+    print("main cancel")
+    t.cancel()
+    print("main sleep")
+    await asyncio.sleep(0.1)
+
+    # Nested: task2 waits on task, task2 is cancelled but task doesn't allow it (task2 should continue)
+    print("----")
+    t = asyncio.create_task(task2(False))
+    await asyncio.sleep(0.01)
+    print("main cancel")
+    t.cancel()
+    print("main sleep")
+    await asyncio.sleep(0.1)
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_cancel_task.py.exp b/tests/asyncio/asyncio_cancel_task.py.exp
new file mode 100644
index 0000000..6a4c206
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_task.py.exp
@@ -0,0 +1,34 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+True
+task start
+True
+main sleep
+task cancel
+task start
+True
+True
+True
+True
+main sleep
+task cancel
+main wait
+main got CancelledError
+task start
+task done
+False
+----
+task 2
+task start
+main cancel
+main sleep
+task cancel
+task 2 cancel
+----
+task 2
+task start
+main cancel
+main sleep
+task cancel
+task 2 done
diff --git a/tests/asyncio/asyncio_cancel_wait_on_finished.py b/tests/asyncio/asyncio_cancel_wait_on_finished.py
new file mode 100644
index 0000000..5ebc934
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_wait_on_finished.py
@@ -0,0 +1,42 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test cancelling a task that is waiting on a task that just finishes.
+import asyncio
+
+
+async def sleep_task():
+    print("sleep_task sleep")
+    await asyncio.sleep(0)
+    print("sleep_task wake")
+
+
+async def wait_task(t):
+    print("wait_task wait")
+    await t
+    print("wait_task wake")
+
+
+async def main():
+    waiting_task = asyncio.create_task(wait_task(asyncio.create_task(sleep_task())))
+
+    print("main sleep")
+    await asyncio.sleep(0)
+    print("main sleep")
+    await asyncio.sleep(0)
+
+    waiting_task.cancel()
+    print("main wait")
+    try:
+        await waiting_task
+    except asyncio.CancelledError as er:
+        print(repr(er))
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_cancel_wait_on_finished.py.exp b/tests/asyncio/asyncio_cancel_wait_on_finished.py.exp
new file mode 100644
index 0000000..7a91c76
--- /dev/null
+++ b/tests/asyncio/asyncio_cancel_wait_on_finished.py.exp
@@ -0,0 +1,10 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+main sleep
+sleep_task sleep
+wait_task wait
+main sleep
+sleep_task wake
+main wait
+CancelledError()
diff --git a/tests/asyncio/asyncio_current_task.py b/tests/asyncio/asyncio_current_task.py
new file mode 100644
index 0000000..c21dc2a
--- /dev/null
+++ b/tests/asyncio/asyncio_current_task.py
@@ -0,0 +1,26 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test current_task() function
+import asyncio
+
+
+async def task(result):
+    result[0] = asyncio.current_task()
+
+
+async def main():
+    result = [None]
+    t = asyncio.create_task(task(result))
+    await asyncio.sleep(0)
+    await asyncio.sleep(0)
+    print(t is result[0])
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_current_task.py.exp b/tests/asyncio/asyncio_current_task.py.exp
new file mode 100644
index 0000000..69f0ff0
--- /dev/null
+++ b/tests/asyncio/asyncio_current_task.py.exp
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+True
diff --git a/tests/asyncio/asyncio_event.py b/tests/asyncio/asyncio_event.py
new file mode 100644
index 0000000..f2247c7
--- /dev/null
+++ b/tests/asyncio/asyncio_event.py
@@ -0,0 +1,99 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test Event class
+import asyncio
+
+
+async def task(id, ev):
+    print("start", id)
+    print(await ev.wait())
+    print("end", id)
+
+
+async def task_delay_set(t, ev):
+    await asyncio.sleep(t)
+    print("set event")
+    ev.set()
+
+
+async def main():
+    ev = asyncio.Event()
+
+    # Set and clear without anything waiting, and test is_set()
+    print(ev.is_set())
+    ev.set()
+    print(ev.is_set())
+    ev.clear()
+    print(ev.is_set())
+
+    # Create 2 tasks waiting on the event
+    print("----")
+    asyncio.create_task(task(1, ev))
+    asyncio.create_task(task(2, ev))
+    print("yield")
+    await asyncio.sleep(0)
+    print("set event")
+    ev.set()
+    print("yield")
+    await asyncio.sleep(0)
+
+    # Create a task waiting on the already-set event
+    print("----")
+    asyncio.create_task(task(3, ev))
+    print("yield")
+    await asyncio.sleep(0)
+
+    # Clear event, start a task, then set event again
+    print("----")
+    print("clear event")
+    ev.clear()
+    asyncio.create_task(task(4, ev))
+    await asyncio.sleep(0)
+    print("set event")
+    ev.set()
+    await asyncio.sleep(0)
+
+    # Cancel a task waiting on an event (set event then cancel task)
+    print("----")
+    ev = asyncio.Event()
+    t = asyncio.create_task(task(5, ev))
+    await asyncio.sleep(0)
+    ev.set()
+    t.cancel()
+    await asyncio.sleep(0.1)
+
+    # Cancel a task waiting on an event (cancel task then set event)
+    print("----")
+    ev = asyncio.Event()
+    t = asyncio.create_task(task(6, ev))
+    await asyncio.sleep(0)
+    t.cancel()
+    ev.set()
+    await asyncio.sleep(0.1)
+
+    # Wait for an event that does get set in time
+    print("----")
+    ev.clear()
+    asyncio.create_task(task_delay_set(0.01, ev))
+    await asyncio.wait_for(ev.wait(), 0.1)
+    await asyncio.sleep(0)
+
+    # Wait for an event that doesn't get set in time
+    print("----")
+    ev.clear()
+    asyncio.create_task(task_delay_set(0.1, ev))
+    try:
+        await asyncio.wait_for(ev.wait(), 0.01)
+    except asyncio.TimeoutError:
+        print("TimeoutError")
+    await ev.wait()
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_event.py.exp b/tests/asyncio/asyncio_event.py.exp
new file mode 100644
index 0000000..348b13c
--- /dev/null
+++ b/tests/asyncio/asyncio_event.py.exp
@@ -0,0 +1,36 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+False
+True
+False
+----
+yield
+start 1
+start 2
+set event
+yield
+True
+end 1
+True
+end 2
+----
+yield
+start 3
+True
+end 3
+----
+clear event
+start 4
+set event
+True
+end 4
+----
+start 5
+----
+start 6
+----
+set event
+----
+TimeoutError
+set event
diff --git a/tests/asyncio/asyncio_event_fair.py b/tests/asyncio/asyncio_event_fair.py
new file mode 100644
index 0000000..00b2986
--- /dev/null
+++ b/tests/asyncio/asyncio_event_fair.py
@@ -0,0 +1,41 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test fairness of Event.set()
+# That tasks which continuously wait on events don't take over the scheduler
+import asyncio
+
+
+async def task1(id):
+    for i in range(4):
+        print("sleep", id)
+        await asyncio.sleep(0)
+
+
+async def task2(id, ev):
+    for i in range(4):
+        ev.set()
+        ev.clear()
+        print("wait", id)
+        await ev.wait()
+
+
+async def main():
+    ev = asyncio.Event()
+    tasks = [
+        asyncio.create_task(task1(0)),
+        asyncio.create_task(task2(2, ev)),
+        asyncio.create_task(task1(1)),
+        asyncio.create_task(task2(3, ev)),
+    ]
+    await tasks[1]
+    ev.set()
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_event_fair.py.exp b/tests/asyncio/asyncio_event_fair.py.exp
new file mode 100644
index 0000000..b7d6d4b
--- /dev/null
+++ b/tests/asyncio/asyncio_event_fair.py.exp
@@ -0,0 +1,19 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+sleep 0
+wait 2
+sleep 1
+wait 3
+sleep 0
+sleep 1
+wait 2
+sleep 0
+sleep 1
+wait 3
+sleep 0
+sleep 1
+wait 2
+wait 3
+wait 2
+wait 3
diff --git a/tests/asyncio/asyncio_exception.py b/tests/asyncio/asyncio_exception.py
new file mode 100644
index 0000000..35b607e
--- /dev/null
+++ b/tests/asyncio/asyncio_exception.py
@@ -0,0 +1,63 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test general exception handling
+import asyncio
+
+
+# main task raising an exception
+async def main():
+    print("main start")
+    raise ValueError(1)
+
+
+try:
+    asyncio.run(main())
+except ValueError as er:
+    print("ValueError", er.args[0])
+
+
+# sub-task raising an exception
+async def task():
+    print("task start")
+    raise ValueError(2)
+    print("task done")
+
+
+async def main():
+    print("main start")
+    t = asyncio.create_task(task())
+    await t
+    print("main done")
+
+
+try:
+    asyncio.run(main())
+except ValueError as er:
+    print("ValueError", er.args[0])
+
+
+# main task raising an exception with sub-task not yet scheduled
+# TODO not currently working, task is never scheduled
+async def task():
+    # print('task run') uncomment this line when it works
+    pass
+
+
+async def main():
+    print("main start")
+    asyncio.create_task(task())
+    raise ValueError(3)
+    print("main done")
+
+
+try:
+    asyncio.run(main())
+except ValueError as er:
+    print("ValueError", er.args[0])
diff --git a/tests/asyncio/asyncio_exception.py.exp b/tests/asyncio/asyncio_exception.py.exp
new file mode 100644
index 0000000..98304ca
--- /dev/null
+++ b/tests/asyncio/asyncio_exception.py.exp
@@ -0,0 +1,10 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+main start
+ValueError 1
+main start
+task start
+ValueError 2
+main start
+ValueError 3
diff --git a/tests/asyncio/asyncio_fair.py b/tests/asyncio/asyncio_fair.py
new file mode 100644
index 0000000..7935555
--- /dev/null
+++ b/tests/asyncio/asyncio_fair.py
@@ -0,0 +1,35 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test fairness of scheduler
+import asyncio
+
+
+async def task(id, t):
+    print("task start", id)
+    while True:
+        if t > 0:
+            print("task work", id)
+        await asyncio.sleep(t)
+
+
+async def main():
+    t1 = asyncio.create_task(task(1, -0.01))
+    t2 = asyncio.create_task(task(2, 0.1))
+    t3 = asyncio.create_task(task(3, 0.18))
+    t4 = asyncio.create_task(task(4, -100))
+    await asyncio.sleep(0.5)
+    t1.cancel()
+    t2.cancel()
+    t3.cancel()
+    t4.cancel()
+    print("finish")
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_fair.py.exp b/tests/asyncio/asyncio_fair.py.exp
new file mode 100644
index 0000000..95af54f
--- /dev/null
+++ b/tests/asyncio/asyncio_fair.py.exp
@@ -0,0 +1,16 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+task start 1
+task start 2
+task work 2
+task start 3
+task work 3
+task start 4
+task work 2
+task work 3
+task work 2
+task work 2
+task work 3
+task work 2
+finish
diff --git a/tests/asyncio/asyncio_gather.py b/tests/asyncio/asyncio_gather.py
new file mode 100644
index 0000000..3a1df60
--- /dev/null
+++ b/tests/asyncio/asyncio_gather.py
@@ -0,0 +1,118 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# test asyncio.gather() function
+import asyncio
+
+
+async def factorial(name, number):
+    f = 1
+    for i in range(2, number + 1):
+        print("Task {}: Compute factorial({})...".format(name, i))
+        await asyncio.sleep(0.01)
+        f *= i
+    print("Task {}: factorial({}) = {}".format(name, number, f))
+    return f
+
+
+async def task(id, t=0.1):
+    print("start", id)
+    await asyncio.sleep(t)
+    print("end", id)
+    return id
+
+
+async def task_loop(id):
+    print("task_loop start", id)
+    while True:
+        await asyncio.sleep(0.1)
+        print("task_loop loop", id)
+
+
+async def task_raise(id, t=0.1):
+    print("task_raise start", id)
+    await asyncio.sleep(t)
+    print("task_raise raise", id)
+    raise ValueError(id)
+
+
+async def gather_task(t0, t1):
+    print("gather_task")
+    await asyncio.gather(t0, t1)
+    print("gather_task2")
+
+
+async def main():
+    # Simple gather with return values
+    print(await asyncio.gather(factorial("A", 2), factorial("B", 3), factorial("C", 4)))
+
+    print("====")
+
+    # Gather with no awaitables
+    print(await asyncio.gather())
+
+    print("====")
+
+    # Test return_exceptions, where one task is cancelled and the other finishes normally
+    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
+    tasks[0].cancel()
+    print(await asyncio.gather(*tasks, return_exceptions=True))
+
+    print("====")
+
+    # Test return_exceptions, where one task raises an exception and the other finishes normally.
+    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task_raise(2))]
+    print(await asyncio.gather(*tasks, return_exceptions=True))
+
+    print("====")
+
+    # Test case where one task raises an exception and other task keeps running.
+    tasks = [asyncio.create_task(task_loop(1)), asyncio.create_task(task_raise(2))]
+    try:
+        await asyncio.gather(*tasks)
+    except ValueError as er:
+        print(repr(er))
+    print(tasks[0].done(), tasks[1].done())
+    for t in tasks:
+        t.cancel()
+    await asyncio.sleep(0.2)
+
+    print("====")
+
+    # Test case where both tasks raise an exception.
+    # Use t=0 so they raise one after the other, between the gather starting and finishing.
+    tasks = [
+        asyncio.create_task(task_raise(1, t=0)),
+        asyncio.create_task(task_raise(2, t=0)),
+    ]
+    try:
+        await asyncio.gather(*tasks)
+    except ValueError as er:
+        print(repr(er))
+    print(tasks[0].done(), tasks[1].done())
+
+    print("====")
+
+    # Cancel a multi gather.
+    t = asyncio.create_task(gather_task(task(1), task(2)))
+    await asyncio.sleep(0.05)
+    t.cancel()
+    await asyncio.sleep(0.2)
+
+    # Test edge cases where the gather is cancelled just as tasks are created and ending.
+    for i in range(1, 4):
+        print("====")
+        t = asyncio.create_task(gather_task(task(1, t=0), task(2, t=0)))
+        for _ in range(i):
+            await asyncio.sleep(0)
+        t.cancel()
+        await asyncio.sleep(0.2)
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_gather.py.exp b/tests/asyncio/asyncio_gather.py.exp
new file mode 100644
index 0000000..c518570
--- /dev/null
+++ b/tests/asyncio/asyncio_gather.py.exp
@@ -0,0 +1,59 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+Task A: Compute factorial(2)...
+Task B: Compute factorial(2)...
+Task C: Compute factorial(2)...
+Task A: factorial(2) = 2
+Task B: Compute factorial(3)...
+Task C: Compute factorial(3)...
+Task B: factorial(3) = 6
+Task C: Compute factorial(4)...
+Task C: factorial(4) = 24
+[2, 6, 24]
+====
+[]
+====
+start 2
+end 2
+[CancelledError(), 2]
+====
+start 1
+task_raise start 2
+end 1
+task_raise raise 2
+[1, ValueError(2,)]
+====
+task_loop start 1
+task_raise start 2
+task_loop loop 1
+task_raise raise 2
+ValueError(2,)
+False True
+====
+task_raise start 1
+task_raise start 2
+task_raise raise 1
+task_raise raise 2
+ValueError(1,)
+True True
+====
+gather_task
+start 1
+start 2
+====
+gather_task
+start 1
+start 2
+====
+gather_task
+start 1
+start 2
+end 1
+end 2
+====
+gather_task
+start 1
+start 2
+end 1
+end 2
diff --git a/tests/asyncio/asyncio_gather_notimpl.py b/tests/asyncio/asyncio_gather_notimpl.py
new file mode 100644
index 0000000..f91cb87
--- /dev/null
+++ b/tests/asyncio/asyncio_gather_notimpl.py
@@ -0,0 +1,66 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test asyncio.gather() function, features that are not implemented.
+import asyncio
+
+
+def custom_handler(loop, context):
+    print(repr(context["exception"]))
+
+
+async def task(id):
+    print("task start", id)
+    await asyncio.sleep(0.01)
+    print("task end", id)
+    return id
+
+
+async def gather_task(t0, t1):
+    print("gather_task start")
+    await asyncio.gather(t0, t1)
+    print("gather_task end")
+
+
+async def main():
+    loop = asyncio.get_event_loop()
+    loop.set_exception_handler(custom_handler)
+
+    # Test case where can't wait on a task being gathered.
+    print("=" * 10)
+    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
+    gt = asyncio.create_task(gather_task(tasks[0], tasks[1]))
+    await asyncio.sleep(0)  # let the gather start
+    try:
+        await tasks[0]  # can't await because this task is part of the gather
+    except RuntimeError as er:
+        print(repr(er))
+    await gt
+
+    # Test case where can't gather on a task being waited.
+    print("=" * 10)
+    tasks = [asyncio.create_task(task(1)), asyncio.create_task(task(2))]
+    asyncio.create_task(gather_task(tasks[0], tasks[1]))
+    await tasks[0]  # wait on this task before the gather starts
+    await tasks[1]
+
+    # Can't gather after a task has completed
+    print("=" * 10)
+    task_1 = asyncio.create_task(task(1))
+
+    try:
+        # Wait for task_1 to complete
+        await task_1
+
+        await asyncio.gather(task_1)
+    except RuntimeError as er:
+        print(repr(er))
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_gather_notimpl.py.exp b/tests/asyncio/asyncio_gather_notimpl.py.exp
new file mode 100644
index 0000000..0609519
--- /dev/null
+++ b/tests/asyncio/asyncio_gather_notimpl.py.exp
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+==========
+task start 1
+task start 2
+gather_task start
+RuntimeError("can't wait",)
+task end 1
+task end 2
+gather_task end
+==========
+task start 1
+task start 2
+gather_task start
+RuntimeError("can't gather",)
+task end 1
+task end 2
+==========
+task start 1
+task end 1
+RuntimeError("can't gather",)
diff --git a/tests/asyncio/asyncio_heaplock.py b/tests/asyncio/asyncio_heaplock.py
new file mode 100644
index 0000000..9515932
--- /dev/null
+++ b/tests/asyncio/asyncio_heaplock.py
@@ -0,0 +1,84 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# test that the following do not use the heap:
+# - basic scheduling of tasks
+# - asyncio.sleep_ms
+# - StreamWriter.write, stream is blocked and data to write is a bytes object
+# - StreamWriter.write, when stream is not blocked
+
+import micropython
+
+# strict stackless builds can't call functions without allocating a frame on the heap
+try:
+    # force bytecode (in case we're running with emit=native) and verify
+    # that bytecode-calling-bytecode doesn't allocate
+    @micropython.bytecode
+    def f(x):
+        x and f(x - 1)
+
+    micropython.heap_lock()
+    f(1)
+    micropython.heap_unlock()
+except RuntimeError:
+    # RuntimeError (max recursion depth) not MemoryError because effectively
+    # the recursion depth is at the limit while the heap is locked with
+    # stackless
+    print("SKIP")
+    raise SystemExit
+
+import asyncio
+
+
+class TestStream:
+    def __init__(self, blocked):
+        self.blocked = blocked
+
+    def write(self, data):
+        print("TestStream.write", data)
+        if self.blocked:
+            return None
+        return len(data)
+
+
+async def task(id, n, t):
+    for i in range(n):
+        print(id, i)
+        await asyncio.sleep_ms(t)
+
+
+async def main():
+    t1 = asyncio.create_task(task(1, 4, 100))
+    t2 = asyncio.create_task(task(2, 2, 250))
+
+    # test scheduling tasks, and calling sleep_ms
+    micropython.heap_lock()
+    print("start")
+    await asyncio.sleep_ms(5)
+    print("sleep")
+    await asyncio.sleep_ms(350)
+    print("finish")
+    micropython.heap_unlock()
+
+    # test writing to a stream, when the underlying stream is blocked
+    s = asyncio.StreamWriter(TestStream(True), None)
+    micropython.heap_lock()
+    s.write(b"12")
+    micropython.heap_unlock()
+
+    # test writing to a stream, when the underlying stream is not blocked
+    buf = bytearray(b"56")
+    s = asyncio.StreamWriter(TestStream(False), None)
+    micropython.heap_lock()
+    s.write(b"34")
+    s.write(buf)
+    micropython.heap_unlock()
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_heaplock.py.exp b/tests/asyncio/asyncio_heaplock.py.exp
new file mode 100644
index 0000000..0551dde
--- /dev/null
+++ b/tests/asyncio/asyncio_heaplock.py.exp
@@ -0,0 +1,15 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+start
+1 0
+2 0
+sleep
+1 1
+1 2
+2 1
+1 3
+finish
+TestStream.write b'12'
+TestStream.write b'34'
+TestStream.write bytearray(b'56')
diff --git a/tests/asyncio/asyncio_lock.py b/tests/asyncio/asyncio_lock.py
new file mode 100644
index 0000000..b8b7758
--- /dev/null
+++ b/tests/asyncio/asyncio_lock.py
@@ -0,0 +1,98 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test Lock class
+import asyncio
+
+
+async def task_loop(id, lock):
+    print("task start", id)
+    for i in range(3):
+        async with lock:
+            print("task have", id, i)
+    print("task end", id)
+
+
+async def task_sleep(lock):
+    async with lock:
+        print("task have", lock.locked())
+        await asyncio.sleep(0.2)
+    print("task release", lock.locked())
+    await lock.acquire()
+    print("task have again")
+    lock.release()
+
+
+async def task_cancel(id, lock, to_cancel=None):
+    try:
+        async with lock:
+            print("task got", id)
+            await asyncio.sleep(0.1)
+        print("task release", id)
+        if to_cancel:
+            to_cancel[0].cancel()
+    except asyncio.CancelledError:
+        print("task cancel", id)
+
+
+async def main():
+    lock = asyncio.Lock()
+
+    # Basic acquire/release
+    print(lock.locked())
+    await lock.acquire()
+    print(lock.locked())
+    await asyncio.sleep(0)
+    lock.release()
+    print(lock.locked())
+    await asyncio.sleep(0)
+
+    # Use with "async with"
+    async with lock:
+        print("have lock")
+
+    # 3 tasks wanting the lock
+    print("----")
+    asyncio.create_task(task_loop(1, lock))
+    asyncio.create_task(task_loop(2, lock))
+    t3 = asyncio.create_task(task_loop(3, lock))
+    await lock.acquire()
+    await asyncio.sleep(0)
+    lock.release()
+    await t3
+
+    # 2 sleeping tasks both wanting the lock
+    print("----")
+    asyncio.create_task(task_sleep(lock))
+    await asyncio.sleep(0.1)
+    await task_sleep(lock)
+
+    # 3 tasks, the first cancelling the second, the third should still run
+    print("----")
+    ts = [None]
+    asyncio.create_task(task_cancel(0, lock, ts))
+    ts[0] = asyncio.create_task(task_cancel(1, lock))
+    asyncio.create_task(task_cancel(2, lock))
+    await asyncio.sleep(0.3)
+    print(lock.locked())
+
+    # 3 tasks, the second and third being cancelled while waiting on the lock
+    print("----")
+    t0 = asyncio.create_task(task_cancel(0, lock))
+    t1 = asyncio.create_task(task_cancel(1, lock))
+    t2 = asyncio.create_task(task_cancel(2, lock))
+    await asyncio.sleep(0.05)
+    t1.cancel()
+    await asyncio.sleep(0.1)
+    t2.cancel()
+    await asyncio.sleep(0.1)
+    print(lock.locked())
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_lock.py.exp b/tests/asyncio/asyncio_lock.py.exp
new file mode 100644
index 0000000..2c4cbac
--- /dev/null
+++ b/tests/asyncio/asyncio_lock.py.exp
@@ -0,0 +1,44 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+False
+True
+False
+have lock
+----
+task start 1
+task start 2
+task start 3
+task have 1 0
+task have 2 0
+task have 3 0
+task have 1 1
+task have 2 1
+task have 3 1
+task have 1 2
+task end 1
+task have 2 2
+task end 2
+task have 3 2
+task end 3
+----
+task have True
+task release False
+task have True
+task release False
+task have again
+task have again
+----
+task got 0
+task release 0
+task cancel 1
+task got 2
+task release 2
+False
+----
+task got 0
+task cancel 1
+task release 0
+task got 2
+task cancel 2
+False
diff --git a/tests/asyncio/asyncio_lock_cancel.py b/tests/asyncio/asyncio_lock_cancel.py
new file mode 100644
index 0000000..59739e4
--- /dev/null
+++ b/tests/asyncio/asyncio_lock_cancel.py
@@ -0,0 +1,56 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test that locks work when cancelling multiple waiters on the lock
+import asyncio
+
+
+async def task(i, lock, lock_flag):
+    print("task", i, "start")
+    try:
+        await lock.acquire()
+    except asyncio.CancelledError:
+        print("task", i, "cancel")
+        return
+    print("task", i, "lock_flag", lock_flag[0])
+    lock_flag[0] = True
+    await asyncio.sleep(0)
+    lock.release()
+    lock_flag[0] = False
+    print("task", i, "done")
+
+
+async def main():
+    # Create a lock and acquire it so the tasks below must wait
+    lock = asyncio.Lock()
+    await lock.acquire()
+    lock_flag = [True]
+
+    # Create 4 tasks and let them all run
+    t0 = asyncio.create_task(task(0, lock, lock_flag))
+    t1 = asyncio.create_task(task(1, lock, lock_flag))
+    t2 = asyncio.create_task(task(2, lock, lock_flag))
+    t3 = asyncio.create_task(task(3, lock, lock_flag))
+    await asyncio.sleep(0)
+
+    # Cancel 2 of the tasks (which are waiting on the lock) and release the lock
+    t1.cancel()
+    t2.cancel()
+    lock.release()
+    lock_flag[0] = False
+
+    # Let the tasks run to completion
+    for _ in range(4):
+        await asyncio.sleep(0)
+
+    # The locke should be unlocked
+    print(lock.locked())
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_lock_cancel.py.exp b/tests/asyncio/asyncio_lock_cancel.py.exp
new file mode 100644
index 0000000..d5decca
--- /dev/null
+++ b/tests/asyncio/asyncio_lock_cancel.py.exp
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+task 0 start
+task 1 start
+task 2 start
+task 3 start
+task 1 cancel
+task 2 cancel
+task 0 lock_flag False
+task 0 done
+task 3 lock_flag False
+task 3 done
+False
diff --git a/tests/asyncio/asyncio_loop_stop.py b/tests/asyncio/asyncio_loop_stop.py
new file mode 100644
index 0000000..1ce3ac6
--- /dev/null
+++ b/tests/asyncio/asyncio_loop_stop.py
@@ -0,0 +1,46 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test Loop.stop() to stop the event loop
+import asyncio
+
+
+async def task():
+    print("task")
+
+
+async def main():
+    print("start")
+
+    # Stop the loop after next yield
+    loop.stop()
+
+    # Check that calling stop() again doesn't do/break anything
+    loop.stop()
+
+    # This await should stop
+    print("sleep")
+    await asyncio.sleep(0)
+
+    # Schedule stop, then create a new task, then yield
+    loop.stop()
+    asyncio.create_task(task())
+    await asyncio.sleep(0)
+
+    # Final stop
+    print("end")
+    loop.stop()
+
+
+loop = asyncio.get_event_loop()
+loop.create_task(main())
+
+for i in range(3):
+    print("run", i)
+    loop.run_forever()
diff --git a/tests/asyncio/asyncio_loop_stop.py.exp b/tests/asyncio/asyncio_loop_stop.py.exp
new file mode 100644
index 0000000..2815fb6
--- /dev/null
+++ b/tests/asyncio/asyncio_loop_stop.py.exp
@@ -0,0 +1,10 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+run 0
+start
+sleep
+run 1
+run 2
+task
+end
diff --git a/tests/asyncio/asyncio_new_event_loop.py b/tests/asyncio/asyncio_new_event_loop.py
new file mode 100644
index 0000000..e5566a6
--- /dev/null
+++ b/tests/asyncio/asyncio_new_event_loop.py
@@ -0,0 +1,37 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test Loop.new_event_loop()
+import asyncio
+
+
+async def task():
+    for i in range(4):
+        print("task", i)
+        await asyncio.sleep(0)
+        await asyncio.sleep(0)
+
+
+async def main():
+    print("start")
+    loop.create_task(task())
+    await asyncio.sleep(0)
+    print("stop")
+    loop.stop()
+
+
+# Use default event loop to run some tasks
+loop = asyncio.get_event_loop()
+loop.create_task(main())
+loop.run_forever()
+
+# Create new event loop, old one should not keep running
+loop = asyncio.new_event_loop()
+loop.create_task(main())
+loop.run_forever()
diff --git a/tests/asyncio/asyncio_new_event_loop.py.exp b/tests/asyncio/asyncio_new_event_loop.py.exp
new file mode 100644
index 0000000..708c24c
--- /dev/null
+++ b/tests/asyncio/asyncio_new_event_loop.py.exp
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+start
+task 0
+stop
+start
+task 0
+stop
diff --git a/tests/asyncio/asyncio_set_exception_handler.py b/tests/asyncio/asyncio_set_exception_handler.py
new file mode 100644
index 0000000..99ad3ab
--- /dev/null
+++ b/tests/asyncio/asyncio_set_exception_handler.py
@@ -0,0 +1,57 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test that tasks return their value correctly to the caller
+import asyncio
+
+
+def custom_handler(loop, context):
+    print("custom_handler", repr(context["exception"]))
+
+
+async def task(i):
+    # Raise with 2 args so exception prints the same in uPy and CPython
+    raise ValueError(i, i + 1)
+
+
+async def main():
+    loop = asyncio.get_event_loop()
+
+    # Check default exception handler, should be None
+    print(loop.get_exception_handler())
+
+    # Set exception handler and test it was set
+    loop.set_exception_handler(custom_handler)
+    print(loop.get_exception_handler() == custom_handler)
+
+    # Create a task that raises and uses the custom exception handler
+    asyncio.create_task(task(0))
+    print("sleep")
+    for _ in range(2):
+        await asyncio.sleep(0)
+
+    # Create 2 tasks to test order of printing exception
+    asyncio.create_task(task(1))
+    asyncio.create_task(task(2))
+    print("sleep")
+    for _ in range(2):
+        await asyncio.sleep(0)
+
+    # Create a task, let it run, then await it (no exception should be printed)
+    t = asyncio.create_task(task(3))
+    await asyncio.sleep(0)
+    try:
+        await t
+    except ValueError as er:
+        print(repr(er))
+
+    print("done")
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_set_exception_handler.py.exp b/tests/asyncio/asyncio_set_exception_handler.py.exp
new file mode 100644
index 0000000..ee777e2
--- /dev/null
+++ b/tests/asyncio/asyncio_set_exception_handler.py.exp
@@ -0,0 +1,12 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+None
+True
+sleep
+custom_handler ValueError(0, 1)
+sleep
+custom_handler ValueError(1, 2)
+custom_handler ValueError(2, 3)
+ValueError(3, 4)
+done
diff --git a/tests/asyncio/asyncio_task_done.py b/tests/asyncio/asyncio_task_done.py
new file mode 100644
index 0000000..aa1b8c1
--- /dev/null
+++ b/tests/asyncio/asyncio_task_done.py
@@ -0,0 +1,67 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test the Task.done() method
+import asyncio
+
+
+async def task(t, exc=None):
+    print("task start")
+    if t >= 0:
+        await asyncio.sleep(t)
+    if exc:
+        raise exc
+    print("task done")
+
+
+async def main():
+    # Task that finishes immediately.
+    print("=" * 10)
+    t = asyncio.create_task(task(-1))
+    print(t.done())
+    await asyncio.sleep(0)
+    print(t.done())
+    await t
+    print(t.done())
+
+    # Task that starts, runs and finishes.
+    print("=" * 10)
+    t = asyncio.create_task(task(0.01))
+    print(t.done())
+    await asyncio.sleep(0)
+    print(t.done())
+    await t
+    print(t.done())
+
+    # Task that raises immediately.
+    print("=" * 10)
+    t = asyncio.create_task(task(-1, ValueError))
+    print(t.done())
+    await asyncio.sleep(0)
+    print(t.done())
+    try:
+        await t
+    except ValueError as er:
+        print(repr(er))
+    print(t.done())
+
+    # Task that raises after a delay.
+    print("=" * 10)
+    t = asyncio.create_task(task(0.01, ValueError))
+    print(t.done())
+    await asyncio.sleep(0)
+    print(t.done())
+    try:
+        await t
+    except ValueError as er:
+        print(repr(er))
+    print(t.done())
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_task_done.py.exp b/tests/asyncio/asyncio_task_done.py.exp
new file mode 100644
index 0000000..97b4cb5
--- /dev/null
+++ b/tests/asyncio/asyncio_task_done.py.exp
@@ -0,0 +1,27 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+==========
+False
+task start
+task done
+True
+True
+==========
+False
+task start
+False
+task done
+True
+==========
+False
+task start
+True
+ValueError()
+True
+==========
+False
+task start
+False
+ValueError()
+True
diff --git a/tests/asyncio/asyncio_wait_for.py b/tests/asyncio/asyncio_wait_for.py
new file mode 100644
index 0000000..f224d66
--- /dev/null
+++ b/tests/asyncio/asyncio_wait_for.py
@@ -0,0 +1,133 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test asyncio.wait_for
+import asyncio
+
+
+async def task(id, t):
+    print("task start", id)
+    await asyncio.sleep(t)
+    print("task end", id)
+    return id * 2
+
+
+async def task_catch():
+    print("task_catch start")
+    try:
+        await asyncio.sleep(0.2)
+    except asyncio.CancelledError:
+        print("ignore cancel")
+    print("task_catch done")
+
+
+async def task_raise():
+    print("task start")
+    raise ValueError
+
+
+async def task_cancel_other(t, other):
+    print("task_cancel_other start")
+    await asyncio.sleep(t)
+    print("task_cancel_other cancel")
+    other.cancel()
+
+
+async def task_wait_for_cancel(id, t, t_wait):
+    print("task_wait_for_cancel start")
+    try:
+        await asyncio.wait_for(task(id, t), t_wait)
+    except asyncio.CancelledError as er:
+        print("task_wait_for_cancel cancelled")
+        raise er
+
+
+async def task_wait_for_cancel_ignore(t_wait):
+    print("task_wait_for_cancel_ignore start")
+    try:
+        await asyncio.wait_for(task_catch(), t_wait)
+    except asyncio.CancelledError as er:
+        print("task_wait_for_cancel_ignore cancelled")
+        raise er
+
+
+async def main():
+    sep = "-" * 10
+
+    # When task finished before the timeout
+    print(await asyncio.wait_for(task(1, 0.01), 10))
+    print(sep)
+
+    # When timeout passes and task is cancelled
+    try:
+        print(await asyncio.wait_for(task(2, 10), 0.01))
+    except asyncio.TimeoutError:
+        print("timeout")
+    print(sep)
+
+    # When timeout passes and task is cancelled, but task ignores the cancellation request
+    try:
+        print(await asyncio.wait_for(task_catch(), 0.1))
+    except asyncio.TimeoutError:
+        print("TimeoutError")
+    print(sep)
+
+    # When task raises an exception
+    try:
+        print(await asyncio.wait_for(task_raise(), 1))
+    except ValueError:
+        print("ValueError")
+    print(sep)
+
+    # Timeout of None means wait forever
+    print(await asyncio.wait_for(task(3, 0.1), None))
+    print(sep)
+
+    # When task is cancelled by another task
+    t = asyncio.create_task(task(4, 10))
+    asyncio.create_task(task_cancel_other(0.01, t))
+    try:
+        print(await asyncio.wait_for(t, 1))
+    except asyncio.CancelledError as er:
+        print(repr(er))
+    print(sep)
+
+    # When wait_for gets cancelled
+    t = asyncio.create_task(task_wait_for_cancel(4, 1, 2))
+    await asyncio.sleep(0.01)
+    t.cancel()
+    await asyncio.sleep(0.01)
+    print(sep)
+
+    # When wait_for gets cancelled and awaited task ignores the cancellation request
+    t = asyncio.create_task(task_wait_for_cancel_ignore(2))
+    await asyncio.sleep(0.01)
+    t.cancel()
+    await asyncio.sleep(0.01)
+    print(sep)
+
+    # When wait_for gets cancelled and the task it's waiting on finishes around the
+    # same time as the cancellation of the wait_for
+    for num_sleep in range(1, 5):
+        t = asyncio.create_task(task_wait_for_cancel(4 + num_sleep, 0, 2))
+        for _ in range(num_sleep):
+            await asyncio.sleep(0)
+        assert not t.done()
+        print("cancel wait_for")
+        t.cancel()
+        try:
+            await t
+        except asyncio.CancelledError as er:
+            print(repr(er))
+        print(sep)
+
+    print("finish")
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_wait_for.py.exp b/tests/asyncio/asyncio_wait_for.py.exp
new file mode 100644
index 0000000..899e560
--- /dev/null
+++ b/tests/asyncio/asyncio_wait_for.py.exp
@@ -0,0 +1,65 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+task start 1
+task end 1
+2
+----------
+task start 2
+timeout
+----------
+task_catch start
+ignore cancel
+task_catch done
+TimeoutError
+----------
+task start
+ValueError
+----------
+task start 3
+task end 3
+6
+----------
+task start 4
+task_cancel_other start
+task_cancel_other cancel
+CancelledError()
+----------
+task_wait_for_cancel start
+task start 4
+task_wait_for_cancel cancelled
+----------
+task_wait_for_cancel_ignore start
+task_catch start
+task_wait_for_cancel_ignore cancelled
+ignore cancel
+task_catch done
+----------
+task_wait_for_cancel start
+cancel wait_for
+task start 5
+task_wait_for_cancel cancelled
+CancelledError()
+----------
+task_wait_for_cancel start
+task start 6
+cancel wait_for
+task end 6
+task_wait_for_cancel cancelled
+CancelledError()
+----------
+task_wait_for_cancel start
+task start 7
+task end 7
+cancel wait_for
+task_wait_for_cancel cancelled
+CancelledError()
+----------
+task_wait_for_cancel start
+task start 8
+task end 8
+cancel wait_for
+task_wait_for_cancel cancelled
+CancelledError()
+----------
+finish
diff --git a/tests/asyncio/asyncio_wait_for_fwd.py b/tests/asyncio/asyncio_wait_for_fwd.py
new file mode 100644
index 0000000..b6b3629
--- /dev/null
+++ b/tests/asyncio/asyncio_wait_for_fwd.py
@@ -0,0 +1,61 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# pylint: skip-file
+#
+# Test asyncio.wait_for, with forwarding cancellation
+import asyncio
+
+
+async def awaiting(t, return_if_fail):
+    try:
+        print("awaiting started")
+        await asyncio.sleep(t)
+    except asyncio.CancelledError as er:
+        # CPython wait_for raises CancelledError inside task but TimeoutError in wait_for
+        print("awaiting canceled")
+        if return_if_fail:
+            return False  # return has no effect if Cancelled
+        else:
+            raise er
+    except Exception as er:
+        print("caught exception", er)
+        raise er
+
+
+async def test_cancellation_forwarded(catch, catch_inside):
+    print("----------")
+
+    async def wait():
+        try:
+            await asyncio.wait_for(awaiting(2, catch_inside), 1)
+        except asyncio.TimeoutError as er:
+            print("Got timeout error")
+            raise er
+        except asyncio.CancelledError as er:
+            print("Got canceled")
+            if not catch:
+                raise er
+
+    async def cancel(t):
+        print("cancel started")
+        await asyncio.sleep(0.01)
+        print("cancel wait()")
+        t.cancel()
+
+    t = asyncio.create_task(wait())
+    k = asyncio.create_task(cancel(t))
+    try:
+        await t
+    except asyncio.CancelledError:
+        print("waiting got cancelled")
+
+
+asyncio.run(test_cancellation_forwarded(False, False))
+asyncio.run(test_cancellation_forwarded(False, True))
+asyncio.run(test_cancellation_forwarded(True, True))
+asyncio.run(test_cancellation_forwarded(True, False))
diff --git a/tests/asyncio/asyncio_wait_for_fwd.py.exp b/tests/asyncio/asyncio_wait_for_fwd.py.exp
new file mode 100644
index 0000000..2cd3705
--- /dev/null
+++ b/tests/asyncio/asyncio_wait_for_fwd.py.exp
@@ -0,0 +1,29 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+----------
+cancel started
+awaiting started
+cancel wait()
+Got canceled
+awaiting canceled
+waiting got cancelled
+----------
+cancel started
+awaiting started
+cancel wait()
+Got canceled
+awaiting canceled
+waiting got cancelled
+----------
+cancel started
+awaiting started
+cancel wait()
+Got canceled
+awaiting canceled
+----------
+cancel started
+awaiting started
+cancel wait()
+Got canceled
+awaiting canceled
diff --git a/tests/asyncio/asyncio_wait_task.py b/tests/asyncio/asyncio_wait_task.py
new file mode 100644
index 0000000..b5c4c8b
--- /dev/null
+++ b/tests/asyncio/asyncio_wait_task.py
@@ -0,0 +1,83 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+# Test waiting on a task
+import asyncio
+
+
+import time
+
+if hasattr(time, "ticks_ms"):
+    ticks = time.ticks_ms
+    ticks_diff = time.ticks_diff
+else:
+
+    def ticks():
+        return int(time.time() * 1000)
+
+    def ticks_diff(ticks_a, ticks_b):
+        return ticks_b - ticks_a
+
+
+async def task(task_id):
+    print("task", task_id)
+
+
+async def delay_print(delay_time, task_id):
+    await asyncio.sleep(delay_time)
+    print(task_id)
+
+
+async def task_raise():
+    print("task_raise")
+    raise ValueError
+
+
+async def main():
+    print("start")
+
+    # Wait on a task
+    task_1 = asyncio.create_task(task(1))
+    await task_1
+
+    # Wait on a task that's already done
+    task_1 = asyncio.create_task(task(2))
+    await asyncio.sleep(0.001)
+    await task_1
+
+    # Wait again on same task
+    await task_1
+
+    print("----")
+
+    # Create 2 tasks
+    task_1 = asyncio.create_task(delay_print(0.2, "hello"))
+    task_2 = asyncio.create_task(delay_print(0.4, "world"))
+
+    # Time how long the tasks take to finish, they should execute in parallel
+    print("start")
+    ticks_1 = ticks()
+    await task_1
+    ticks_2 = ticks()
+    await task_2
+    ticks_3 = ticks()
+    print(
+        "took {} {}".format(
+            round(ticks_diff(ticks_2, ticks_1), -2),
+            round(ticks_diff(ticks_3, ticks_2), -2),
+        )
+    )
+
+    # Wait on a task that raises an exception
+    task_1 = asyncio.create_task(task_raise())
+    try:
+        await task_1
+    except ValueError:
+        print("ValueError")
+
+
+asyncio.run(main())
diff --git a/tests/asyncio/asyncio_wait_task.py.exp b/tests/asyncio/asyncio_wait_task.py.exp
new file mode 100644
index 0000000..65db976
--- /dev/null
+++ b/tests/asyncio/asyncio_wait_task.py.exp
@@ -0,0 +1,13 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+start
+task 1
+task 2
+----
+start
+hello
+world
+took 200 200
+task_raise
+ValueError
diff --git a/tests/run_tests.py b/tests/run_tests.py
new file mode 100755
index 0000000..35be22b
--- /dev/null
+++ b/tests/run_tests.py
@@ -0,0 +1,138 @@
+#! /usr/bin/env python3
+
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+#
+
+import sys
+import os
+
+try:
+    from typing import List, Tuple
+except ImportError:
+    pass
+
+AVAILABLE_SUITES = ["asyncio"]
+
+
+LICENSE_PREFIX = """# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+"""
+
+
+def get_interpreter():
+    interpreter = os.getenv("MICROPY_MICROPYTHON")
+
+    if interpreter:
+        return interpreter
+
+    if sys.platform == "win32":
+        return "micropython.exe"
+
+    return "micropython"
+
+
+def get_testcases(suite: str) -> List[str]:
+    if sys.platform == "win32":
+        # dir /b prints only contained filenames, one on a line
+        # http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/dir.mspx
+        result = os.system("dir /b %s/*.py >tests.lst" % suite)
+    else:
+        result = os.system("ls %s/*.py | xargs -n1 basename >tests.lst" % suite)
+
+    assert result == 0
+
+    with open("tests.lst") as test_list_file:
+        testcases = test_list_file.readlines()
+        testcases = [l[:-1] for l in testcases]
+
+    os.system("rm tests.lst")
+    assert testcases, "No tests found in dir '%s', which is implausible" % suite
+
+    return testcases
+
+
+def run_testcase(suite: str, testcase: str):
+    qtest = "%s/%s" % (suite, testcase)
+
+    try:
+        with open("%s.exp" % qtest) as expected_output_file:
+            expected_output = expected_output_file.read()
+    except OSError as exc:
+        raise RuntimeError("SKIP") from exc
+
+    with open("{0}.out".format(qtest), "w") as actual_output_file:
+        actual_output_file.write(LICENSE_PREFIX)
+
+    result = os.system("{0} {1} >> {1}.out 2>&1".format(get_interpreter(), qtest))
+
+    with open("%s.out" % qtest) as actual_output_file:
+        actual_output = actual_output_file.read()
+
+    if result != 0:
+        actual_output += "\n\nCRASH\n"
+
+    if actual_output == LICENSE_PREFIX + "SKIP\n":
+        print("skip %s" % qtest)
+        raise RuntimeError("SKIP")
+
+    if actual_output != expected_output:
+        print("FAIL %s" % qtest)
+        os.system("diff -u {0}.exp {0}.out".format(qtest))
+        return False
+
+    print("pass %s" % qtest)
+    return True
+
+
+def run_suite(suite: str) -> Tuple[int, int, int]:
+    test_count = 0
+    passed_count = 0
+    skip_count = 0
+
+    testcases = get_testcases(suite)
+
+    for testcase in testcases:
+        try:
+            if run_testcase(suite, testcase):
+                passed_count += 1
+
+            test_count += 1
+        except RuntimeError as exc:
+            if str(exc) == "SKIP":
+                skip_count += 1
+
+    return test_count, passed_count, skip_count
+
+
+def main():
+    test_count = 0
+    passed_count = 0
+    skip_count = 0
+
+    for suite in AVAILABLE_SUITES:
+        suite_test_count, suite_passed_count, suite_skip_count = run_suite(suite)
+
+        test_count += suite_test_count
+        passed_count += suite_passed_count
+        skip_count += suite_skip_count
+
+    print("-" * 20)
+    print("%s tests performed" % test_count)
+    print("%s tests passed" % passed_count)
+    if test_count != passed_count:
+        print("%s tests failed" % (test_count - passed_count))
+    if skip_count:
+        print("%s tests skipped" % skip_count)
+
+    if test_count - passed_count > 0:
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py
new file mode 100644
index 0000000..9f0d2b3
--- /dev/null
+++ b/tests/test_placeholder.py
@@ -0,0 +1,13 @@
+# SPDX-FileCopyrightText: 2019 Damien P. George
+#
+# SPDX-License-Identifier: MIT
+#
+# MicroPython uasyncio module
+# MIT license; Copyright (c) 2019 Damien P. George
+
+
+def test_placeholder():
+    # Because we have the tests directory, the CI will attempt to run pytest
+    # and fail because no tests exist.  By adding a placeholder test that
+    # we can avoid that without having to avoid the directory name `tests`
+    pass