Repository: exercism/python
Pull request #2256 (2020-10-09 18:02:51)

Bringing your track in line with the latest changes to Problem Specifications
We're in the process of re-opening the [Problem Specifications repo](https://github.com/exercism/problem-specifications), as discussed in [this issue](https://github.com/exercism/problem-specifications/issues/1674).
This PR adds `.meta/tests.toml` files for all exercises for which canonical data is defined in the Problem Specifications repo.
We'll now discuss _why_ we're making this change.
## Keeping track of implemented tests
While most of the changes to the Problem Specifications repo are specific to that repo, there is one track-specific change:
> If a track implements an exercise for which test data exists, the exercise _must_ contain a `.meta/tests.toml` file.
The goal of the `tests.toml` file is to keep track of which tests are implemented by the exercise.
Tests in this file are identified by their UUID and each test has a boolean value that indicates if it is implemented by that exercise.
A `tests.toml` file for a track's `two-fer` exercise looks like this:
```toml
[canonical-tests]
# no name given
"19709124-b82e-4e86-a722-9e5c5ebf3952" = true
# a name given
"3451eebd-123f-4256-b667-7b109affce32" = true
# another name given
"653611c6-be9f-4935-ab42-978e25fe9a10" = false
```
In this case, the track has chosen to implement two of the three available tests.
If a track uses a _test generator_ to generate an exercise's test suite, it _must_ use the contents of the `tests.toml` file to determine which tests to include in the generated test suite.
## Tooling
To make it easy to keep the `tests.toml` up to date, tracks can use the [`canonical_data_syncer` application](https://github.com/exercism/canonical-data-syncer).
This application is a small, standalone binary that will compare the tests specified in the `tests.toml` files against the tests that are defined in the exercise's canonical data.
It then interactively gives the maintainer the option to include or exclude test cases that are currently missing, updating the `tests.toml` file accordingly.
To use the canonical data syncer tool, tracks should copying the [`fetch-canonical_data_syncer`](https://github.com/exercism/canonical-data-syncer/blob/master/scripts/fetch-canonical_data_syncer) and/or [`fetch-canonical_data_syncer.ps1`](https://github.com/exercism/canonical-data-syncer/blob/master/scripts/fetch-canonical_data_syncer.ps1) scripts into their repository.
Then, running either of these scripts will download the latest version of the tool to the track's `bin` directory.
The tool can be run using `./bin/canonical_data_syncer` or `.\bin\canonical_data_syncer.exe`, depending on your operating system.
## Changes
In this PR, we're adding `meta/tests.toml` files for all the exercises for which canonical data is defined.
We've initially marked all tests as _included_, which means that we'll assume that your track has implemented those tests.
If there isn't anything obviously wrong with the PR, I would suggest to merge this PR first, and then later on update the `meta/tests.toml` files according to your track's actual implementation.



Patch:
diff --git a/config.json b/config.json
index 75ebd28b..0415e630 100644
--- a/config.json
+++ b/config.json
@@ -1482,6 +1482,18 @@
         "loops"
       ]
     },
+    {
+      "slug": "paasio",
+      "uuid": "ec36f9ed-f376-43d9-90ee-be0bea0cfc98",
+      "core": false,
+      "unlocked_by": "book-store",
+      "difficulty": 5,
+      "topics": [
+        "classes",
+        "interfaces",
+        "networking"
+      ]
+    },
     {
       "slug": "accumulate",
       "uuid": "e7351e8e-d3ff-4621-b818-cd55cf05bffd",
diff --git a/exercises/paasio/README.md b/exercises/paasio/README.md
new file mode 100644
index 00000000..a031f124
--- /dev/null
+++ b/exercises/paasio/README.md
@@ -0,0 +1,60 @@
+# PaaS I/O
+
+Report network IO statistics.
+
+You are writing a [PaaS][], and you need a way to bill customers based
+on network and filesystem usage.
+
+Create a wrapper for network connections and files that can report IO
+statistics. The wrapper must report:
+
+- The total number of bytes read/written.
+- The total number of read/write operations.
+
+[PaaS]: http://en.wikipedia.org/wiki/Platform_as_a_service
+
+## Exception messages
+
+Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to
+indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not
+every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
+a message.
+
+To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
+`raise Exception`, you should write:
+
+```python
+raise Exception("Meaningful message indicating the source of the error")
+```
+
+## Running the tests
+
+To run the tests, run `pytest paasio_test.py`
+
+Alternatively, you can tell Python to run the pytest module:
+`python -m pytest paasio_test.py`
+
+### Common `pytest` options
+
+- `-v` : enable verbose output
+- `-x` : stop running tests on first failure
+- `--ff` : run failures from previous test before running other test cases
+
+For other options, see `python -m pytest -h`
+
+## Submitting Exercises
+
+Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/paasio` directory.
+
+You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
+
+For more detailed information about running tests, code style and linting,
+please see [Running the Tests](http://exercism.io/tracks/python/tests).
+
+## Source
+
+Brian Matsuo [https://github.com/bmatsuo](https://github.com/bmatsuo)
+
+## Submitting Incomplete Solutions
+
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
diff --git a/exercises/paasio/example.py b/exercises/paasio/example.py
new file mode 100644
index 00000000..54a404a6
--- /dev/null
+++ b/exercises/paasio/example.py
@@ -0,0 +1,102 @@
+import io
+
+
+class MeteredFile(io.BufferedRandom):
+    """Implement using a subclassing model."""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._read_bytes = 0
+        self._read_ops = 0
+        self._write_bytes = 0
+        self._write_ops = 0
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        return super().__exit__(exc_type, exc_val, exc_tb)
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        self._read_ops += 1
+        data = super().readline()
+        self._read_bytes += len(data)
+        if data:
+            return data
+        raise StopIteration
+
+    def read(self, size=-1):
+        self._read_ops += 1
+        data = super().read(size)
+        self._read_bytes += len(data)
+        return data
+
+    @property
+    def read_bytes(self):
+        return self._read_bytes
+
+    @property
+    def read_ops(self):
+        return self._read_ops
+
+    def write(self, b):
+        self._write_ops += 1
+        length = super().write(b)
+        self._write_bytes += length
+        return length
+
+    @property
+    def write_bytes(self):
+        return self._write_bytes
+
+    @property
+    def write_ops(self):
+        return self._write_ops
+
+
+class MeteredSocket:
+    """Implement using a delegation model."""
+
+    def __init__(self, socket):
+        self._socket = socket
+        self._recv_bytes = 0
+        self._recv_ops = 0
+        self._send_bytes = 0
+        self._send_ops = 0
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        return self._socket.__exit__(exc_type, exc_val, exc_tb)
+
+    def recv(self, bufsize, flags=0):
+        self._recv_ops += 1
+        data = self._socket.recv(bufsize, flags)
+        self._recv_bytes += len(data)
+        return data
+
+    @property
+    def recv_bytes(self):
+        return self._recv_bytes
+
+    @property
+    def recv_ops(self):
+        return self._recv_ops
+
+    def send(self, data, flags=0):
+        self._send_ops += 1
+        length = self._socket.send(data, flags)
+        self._send_bytes += length
+        return length
+
+    @property
+    def send_bytes(self):
+        return self._send_bytes
+
+    @property
+    def send_ops(self):
+        return self._send_ops
diff --git a/exercises/paasio/paasio.py b/exercises/paasio/paasio.py
new file mode 100644
index 00000000..62987c6a
--- /dev/null
+++ b/exercises/paasio/paasio.py
@@ -0,0 +1,77 @@
+import io
+
+
+class MeteredFile(io.BufferedRandom):
+    """Implement using a subclassing model."""
+
+    def __init__(self, *args, **kwargs):
+        pass
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        pass
+
+    def __iter__(self):
+        pass
+
+    def __next__(self):
+        pass
+
+    def read(self, size=-1):
+        pass
+
+    @property
+    def read_bytes(self):
+        pass
+
+    @property
+    def read_ops(self):
+        pass
+
+    def write(self, b):
+        pass
+
+    @property
+    def write_bytes(self):
+        pass
+
+    @property
+    def write_ops(self):
+        pass
+
+
+class MeteredSocket:
+    """Implement using a delegation model."""
+
+    def __init__(self, socket):
+        pass
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        pass
+
+    def recv(self, bufsize, flags=0):
+        pass
+
+    @property
+    def recv_bytes(self):
+        pass
+
+    @property
+    def recv_ops(self):
+        pass
+
+    def send(self, data, flags=0):
+        pass
+
+    @property
+    def send_bytes(self):
+        pass
+
+    @property
+    def send_ops(self):
+        pass


Test patch:
diff --git a/exercises/acronym/.meta/tests.toml b/exercises/acronym/.meta/tests.toml
new file mode 100644
index 00000000..33af0489
--- /dev/null
+++ b/exercises/acronym/.meta/tests.toml
@@ -0,0 +1,28 @@
+[canonical-tests]
+
+# basic
+"1e22cceb-c5e4-4562-9afe-aef07ad1eaf4" = true
+
+# lowercase words
+"79ae3889-a5c0-4b01-baf0-232d31180c08" = true
+
+# punctuation
+"ec7000a7-3931-4a17-890e-33ca2073a548" = true
+
+# all caps word
+"32dd261c-0c92-469a-9c5c-b192e94a63b0" = true
+
+# punctuation without whitespace
+"ae2ac9fa-a606-4d05-8244-3bcc4659c1d4" = true
+
+# very long abbreviation
+"0e4b1e7c-1a6d-48fb-81a7-bf65eb9e69f9" = true
+
+# consecutive delimiters
+"6a078f49-c68d-4b7b-89af-33a1a98c28cc" = true
+
+# apostrophes
+"5118b4b1-4572-434c-8d57-5b762e57973e" = true
+
+# underscore emphasis
+"adc12eab-ec2d-414f-b48c-66a4fc06cdef" = true
diff --git a/exercises/affine-cipher/.meta/tests.toml b/exercises/affine-cipher/.meta/tests.toml
new file mode 100644
index 00000000..4eb72947
--- /dev/null
+++ b/exercises/affine-cipher/.meta/tests.toml
@@ -0,0 +1,49 @@
+[canonical-tests]
+
+# encode yes
+"2ee1d9af-1c43-416c-b41b-cefd7d4d2b2a" = true
+
+# encode no
+"785bade9-e98b-4d4f-a5b0-087ba3d7de4b" = true
+
+# encode OMG
+"2854851c-48fb-40d8-9bf6-8f192ed25054" = true
+
+# encode O M G
+"bc0c1244-b544-49dd-9777-13a770be1bad" = true
+
+# encode mindblowingly
+"381a1a20-b74a-46ce-9277-3778625c9e27" = true
+
+# encode numbers
+"6686f4e2-753b-47d4-9715-876fdc59029d" = true
+
+# encode deep thought
+"ae23d5bd-30a8-44b6-afbe-23c8c0c7faa3" = true
+
+# encode all the letters
+"c93a8a4d-426c-42ef-9610-76ded6f7ef57" = true
+
+# encode with a not coprime to m
+"0673638a-4375-40bd-871c-fb6a2c28effb" = true
+
+# decode exercism
+"3f0ac7e2-ec0e-4a79-949e-95e414953438" = true
+
+# decode a sentence
+"241ee64d-5a47-4092-a5d7-7939d259e077" = true
+
+# decode numbers
+"33fb16a1-765a-496f-907f-12e644837f5e" = true
+
+# decode all the letters
+"20bc9dce-c5ec-4db6-a3f1-845c776bcbf7" = true
+
+# decode with no spaces in input
+"623e78c0-922d-49c5-8702-227a3e8eaf81" = true
+
+# decode with too many spaces
+"58fd5c2a-1fd9-4563-a80a-71cff200f26f" = true
+
+# decode with a not coprime to m
+"b004626f-c186-4af9-a3f4-58f74cdb86d5" = true
diff --git a/exercises/all-your-base/.meta/tests.toml b/exercises/all-your-base/.meta/tests.toml
new file mode 100644
index 00000000..363f95a2
--- /dev/null
+++ b/exercises/all-your-base/.meta/tests.toml
@@ -0,0 +1,64 @@
+[canonical-tests]
+
+# single bit one to decimal
+"5ce422f9-7a4b-4f44-ad29-49c67cb32d2c" = true
+
+# binary to single decimal
+"0cc3fea8-bb79-46ac-a2ab-5a2c93051033" = true
+
+# single decimal to binary
+"f12db0f9-0d3d-42c2-b3ba-e38cb375a2b8" = true
+
+# binary to multiple decimal
+"2c45cf54-6da3-4748-9733-5a3c765d925b" = true
+
+# decimal to binary
+"65ddb8b4-8899-4fcc-8618-181b2cf0002d" = true
+
+# trinary to hexadecimal
+"8d418419-02a7-4824-8b7a-352d33c6987e" = true
+
+# hexadecimal to trinary
+"d3901c80-8190-41b9-bd86-38d988efa956" = true
+
+# 15-bit integer
+"5d42f85e-21ad-41bd-b9be-a3e8e4258bbf" = true
+
+# empty list
+"d68788f7-66dd-43f8-a543-f15b6d233f83" = true
+
+# single zero
+"5e27e8da-5862-4c5f-b2a9-26c0382b6be7" = true
+
+# multiple zeros
+"2e1c2573-77e4-4b9c-8517-6c56c5bcfdf2" = true
+
+# leading zeros
+"3530cd9f-8d6d-43f5-bc6e-b30b1db9629b" = true
+
+# input base is one
+"a6b476a1-1901-4f2a-92c4-4d91917ae023" = true
+
+# input base is zero
+"e21a693a-7a69-450b-b393-27415c26a016" = true
+
+# input base is negative
+"54a23be5-d99e-41cc-88e0-a650ffe5fcc2" = true
+
+# negative digit
+"9eccf60c-dcc9-407b-95d8-c37b8be56bb6" = true
+
+# invalid positive digit
+"232fa4a5-e761-4939-ba0c-ed046cd0676a" = true
+
+# output base is one
+"14238f95-45da-41dc-95ce-18f860b30ad3" = true
+
+# output base is zero
+"73dac367-da5c-4a37-95fe-c87fad0a4047" = true
+
+# output base is negative
+"13f81f42-ff53-4e24-89d9-37603a48ebd9" = true
+
+# both bases are negative
+"0e6c895d-8a5d-4868-a345-309d094cfe8d" = true
diff --git a/exercises/allergies/.meta/tests.toml b/exercises/allergies/.meta/tests.toml
new file mode 100644
index 00000000..f87992bb
--- /dev/null
+++ b/exercises/allergies/.meta/tests.toml
@@ -0,0 +1,148 @@
+[canonical-tests]
+
+# not allergic to anything
+"17fc7296-2440-4ac4-ad7b-d07c321bc5a0" = true
+
+# allergic only to eggs
+"07ced27b-1da5-4c2e-8ae2-cb2791437546" = true
+
+# allergic to eggs and something else
+"5035b954-b6fa-4b9b-a487-dae69d8c5f96" = true
+
+# allergic to something, but not eggs
+"64a6a83a-5723-4b5b-a896-663307403310" = true
+
+# allergic to everything
+"90c8f484-456b-41c4-82ba-2d08d93231c6" = true
+
+# not allergic to anything
+"d266a59a-fccc-413b-ac53-d57cb1f0db9d" = true
+
+# allergic only to peanuts
+"ea210a98-860d-46b2-a5bf-50d8995b3f2a" = true
+
+# allergic to peanuts and something else
+"eac69ae9-8d14-4291-ac4b-7fd2c73d3a5b" = true
+
+# allergic to something, but not peanuts
+"9152058c-ce39-4b16-9b1d-283ec6d25085" = true
+
+# allergic to everything
+"d2d71fd8-63d5-40f9-a627-fbdaf88caeab" = true
+
+# not allergic to anything
+"b948b0a1-cbf7-4b28-a244-73ff56687c80" = true
+
+# allergic only to shellfish
+"9ce9a6f3-53e9-4923-85e0-73019047c567" = true
+
+# allergic to shellfish and something else
+"b272fca5-57ba-4b00-bd0c-43a737ab2131" = true
+
+# allergic to something, but not shellfish
+"21ef8e17-c227-494e-8e78-470a1c59c3d8" = true
+
+# allergic to everything
+"cc789c19-2b5e-4c67-b146-625dc8cfa34e" = true
+
+# not allergic to anything
+"651bde0a-2a74-46c4-ab55-02a0906ca2f5" = true
+
+# allergic only to strawberries
+"b649a750-9703-4f5f-b7f7-91da2c160ece" = true
+
+# allergic to strawberries and something else
+"50f5f8f3-3bac-47e6-8dba-2d94470a4bc6" = true
+
+# allergic to something, but not strawberries
+"23dd6952-88c9-48d7-a7d5-5d0343deb18d" = true
+
+# allergic to everything
+"74afaae2-13b6-43a2-837a-286cd42e7d7e" = true
+
+# not allergic to anything
+"c49a91ef-6252-415e-907e-a9d26ef61723" = true
+
+# allergic only to tomatoes
+"b69c5131-b7d0-41ad-a32c-e1b2cc632df8" = true
+
+# allergic to tomatoes and something else
+"1ca50eb1-f042-4ccf-9050-341521b929ec" = true
+
+# allergic to something, but not tomatoes
+"e9846baa-456b-4eff-8025-034b9f77bd8e" = true
+
+# allergic to everything
+"b2414f01-f3ad-4965-8391-e65f54dad35f" = true
+
+# not allergic to anything
+"978467ab-bda4-49f7-b004-1d011ead947c" = true
+
+# allergic only to chocolate
+"59cf4e49-06ea-4139-a2c1-d7aad28f8cbc" = true
+
+# allergic to chocolate and something else
+"b0a7c07b-2db7-4f73-a180-565e07040ef1" = true
+
+# allergic to something, but not chocolate
+"f5506893-f1ae-482a-b516-7532ba5ca9d2" = true
+
+# allergic to everything
+"02debb3d-d7e2-4376-a26b-3c974b6595c6" = true
+
+# not allergic to anything
+"17f4a42b-c91e-41b8-8a76-4797886c2d96" = true
+
+# allergic only to pollen
+"7696eba7-1837-4488-882a-14b7b4e3e399" = true
+
+# allergic to pollen and something else
+"9a49aec5-fa1f-405d-889e-4dfc420db2b6" = true
+
+# allergic to something, but not pollen
+"3cb8e79f-d108-4712-b620-aa146b1954a9" = true
+
+# allergic to everything
+"1dc3fe57-7c68-4043-9d51-5457128744b2" = true
+
+# not allergic to anything
+"d3f523d6-3d50-419b-a222-d4dfd62ce314" = true
+
+# allergic only to cats
+"eba541c3-c886-42d3-baef-c048cb7fcd8f" = true
+
+# allergic to cats and something else
+"ba718376-26e0-40b7-bbbe-060287637ea5" = true
+
+# allergic to something, but not cats
+"3c6dbf4a-5277-436f-8b88-15a206f2d6c4" = true
+
+# allergic to everything
+"1faabb05-2b98-4995-9046-d83e4a48a7c1" = true
+
+# no allergies
+"f9c1b8e7-7dc5-4887-aa93-cebdcc29dd8f" = true
+
+# just eggs
+"9e1a4364-09a6-4d94-990f-541a94a4c1e8" = true
+
+# just peanuts
+"8851c973-805e-4283-9e01-d0c0da0e4695" = true
+
+# just strawberries
+"2c8943cb-005e-435f-ae11-3e8fb558ea98" = true
+
+# eggs and peanuts
+"6fa95d26-044c-48a9-8a7b-9ee46ec32c5c" = true
+
+# more than eggs but not peanuts
+"19890e22-f63f-4c5c-a9fb-fb6eacddfe8e" = true
+
+# lots of stuff
+"4b68f470-067c-44e4-889f-c9fe28917d2f" = true
+
+# everything
+"0881b7c5-9efa-4530-91bd-68370d054bc7" = true
+
+# no allergen score parts
+"12ce86de-b347-42a0-ab7c-2e0570f0c65b" = true
diff --git a/exercises/alphametics/.meta/tests.toml b/exercises/alphametics/.meta/tests.toml
new file mode 100644
index 00000000..4e0ea377
--- /dev/null
+++ b/exercises/alphametics/.meta/tests.toml
@@ -0,0 +1,31 @@
+[canonical-tests]
+
+# puzzle with three letters
+"e0c08b07-9028-4d5f-91e1-d178fead8e1a" = true
+
+# solution must have unique value for each letter
+"a504ee41-cb92-4ec2-9f11-c37e95ab3f25" = true
+
+# leading zero solution is invalid
+"4e3b81d2-be7b-4c5c-9a80-cd72bc6d465a" = true
+
+# puzzle with two digits final carry
+"8a3e3168-d1ee-4df7-94c7-b9c54845ac3a" = true
+
+# puzzle with four letters
+"a9630645-15bd-48b6-a61e-d85c4021cc09" = true
+
+# puzzle with six letters
+"3d905a86-5a52-4e4e-bf80-8951535791bd" = true
+
+# puzzle with seven letters
+"4febca56-e7b7-4789-97b9-530d09ba95f0" = true
+
+# puzzle with eight letters
+"12125a75-7284-4f9a-a5fa-191471e0d44f" = true
+
+# puzzle with ten letters
+"fb05955f-38dc-477a-a0b6-5ef78969fffa" = true
+
+# puzzle with ten letters and 199 addends
+"9a101e81-9216-472b-b458-b513a7adacf7" = true
diff --git a/exercises/anagram/.meta/tests.toml b/exercises/anagram/.meta/tests.toml
new file mode 100644
index 00000000..4416dce9
--- /dev/null
+++ b/exercises/anagram/.meta/tests.toml
@@ -0,0 +1,43 @@
+[canonical-tests]
+
+# no matches
+"dd40c4d2-3c8b-44e5-992a-f42b393ec373" = true
+
+# detects two anagrams
+"b3cca662-f50a-489e-ae10-ab8290a09bdc" = true
+
+# does not detect anagram subsets
+"a27558ee-9ba0-4552-96b1-ecf665b06556" = true
+
+# detects anagram
+"64cd4584-fc15-4781-b633-3d814c4941a4" = true
+
+# detects three anagrams
+"99c91beb-838f-4ccd-b123-935139917283" = true
+
+# detects multiple anagrams with different case
+"78487770-e258-4e1f-a646-8ece10950d90" = true
+
+# does not detect non-anagrams with identical checksum
+"1d0ab8aa-362f-49b7-9902-3d0c668d557b" = true
+
+# detects anagrams case-insensitively
+"9e632c0b-c0b1-4804-8cc1-e295dea6d8a8" = true
+
+# detects anagrams using case-insensitive subject
+"b248e49f-0905-48d2-9c8d-bd02d8c3e392" = true
+
+# detects anagrams using case-insensitive possible matches
+"f367325c-78ec-411c-be76-e79047f4bd54" = true
+
+# does not detect an anagram if the original word is repeated
+"7cc195ad-e3c7-44ee-9fd2-d3c344806a2c" = true
+
+# anagrams must use all letters exactly once
+"9878a1c9-d6ea-4235-ae51-3ea2befd6842" = true
+
+# words are not anagrams of themselves (case-insensitive)
+"85757361-4535-45fd-ac0e-3810d40debc1" = true
+
+# words other than themselves can be anagrams
+"a0705568-628c-4b55-9798-82e4acde51ca" = true
diff --git a/exercises/armstrong-numbers/.meta/tests.toml b/exercises/armstrong-numbers/.meta/tests.toml
new file mode 100644
index 00000000..7358d6f1
--- /dev/null
+++ b/exercises/armstrong-numbers/.meta/tests.toml
@@ -0,0 +1,28 @@
+[canonical-tests]
+
+# Zero is an Armstrong number
+"c1ed103c-258d-45b2-be73-d8c6d9580c7b" = true
+
+# Single digit numbers are Armstrong numbers
+"579e8f03-9659-4b85-a1a2-d64350f6b17a" = true
+
+# There are no 2 digit Armstrong numbers
+"2d6db9dc-5bf8-4976-a90b-b2c2b9feba60" = true
+
+# Three digit number that is an Armstrong number
+"509c087f-e327-4113-a7d2-26a4e9d18283" = true
+
+# Three digit number that is not an Armstrong number
+"7154547d-c2ce-468d-b214-4cb953b870cf" = true
+
+# Four digit number that is an Armstrong number
+"6bac5b7b-42e9-4ecb-a8b0-4832229aa103" = true
+
+# Four digit number that is not an Armstrong number
+"eed4b331-af80-45b5-a80b-19c9ea444b2e" = true
+
+# Seven digit number that is an Armstrong number
+"f971ced7-8d68-4758-aea1-d4194900b864" = true
+
+# Seven digit number that is not an Armstrong number
+"7ee45d52-5d35-4fbd-b6f1-5c8cd8a67f18" = true
diff --git a/exercises/atbash-cipher/.meta/tests.toml b/exercises/atbash-cipher/.meta/tests.toml
new file mode 100644
index 00000000..22b1e97a
--- /dev/null
+++ b/exercises/atbash-cipher/.meta/tests.toml
@@ -0,0 +1,43 @@
+[canonical-tests]
+
+# encode yes
+"2f47ebe1-eab9-4d6b-b3c6-627562a31c77" = true
+
+# encode no
+"b4ffe781-ea81-4b74-b268-cc58ba21c739" = true
+
+# encode OMG
+"10e48927-24ab-4c4d-9d3f-3067724ace00" = true
+
+# encode spaces
+"d59b8bc3-509a-4a9a-834c-6f501b98750b" = true
+
+# encode mindblowingly
+"31d44b11-81b7-4a94-8b43-4af6a2449429" = true
+
+# encode numbers
+"d503361a-1433-48c0-aae0-d41b5baa33ff" = true
+
+# encode deep thought
+"79c8a2d5-0772-42d4-b41b-531d0b5da926" = true
+
+# encode all the letters
+"9ca13d23-d32a-4967-a1fd-6100b8742bab" = true
+
+# decode exercism
+"bb50e087-7fdf-48e7-9223-284fe7e69851" = true
+
+# decode a sentence
+"ac021097-cd5d-4717-8907-b0814b9e292c" = true
+
+# decode numbers
+"18729de3-de74-49b8-b68c-025eaf77f851" = true
+
+# decode all the letters
+"0f30325f-f53b-415d-ad3e-a7a4f63de034" = true
+
+# decode with too many spaces
+"39640287-30c6-4c8c-9bac-9d613d1a5674" = true
+
+# decode with no spaces
+"b34edf13-34c0-49b5-aa21-0768928000d5" = true
diff --git a/exercises/beer-song/.meta/tests.toml b/exercises/beer-song/.meta/tests.toml
new file mode 100644
index 00000000..b0f4ffbf
--- /dev/null
+++ b/exercises/beer-song/.meta/tests.toml
@@ -0,0 +1,25 @@
+[canonical-tests]
+
+# first generic verse
+"5a02fd08-d336-4607-8006-246fe6fa9fb0" = true
+
+# last generic verse
+"77299ca6-545e-4217-a9cc-606b342e0187" = true
+
+# verse with 2 bottles
+"102cbca0-b197-40fd-b548-e99609b06428" = true
+
+# verse with 1 bottle
+"b8ef9fce-960e-4d85-a0c9-980a04ec1972" = true
+
+# verse with 0 bottles
+"c59d4076-f671-4ee3-baaa-d4966801f90d" = true
+
+# first two verses
+"7e17c794-402d-4ca6-8f96-4d8f6ee1ec7e" = true
+
+# last three verses
+"949868e7-67e8-43d3-9bb4-69277fe020fb" = true
+
+# all verses
+"bc220626-126c-4e72-8df4-fddfc0c3e458" = true
diff --git a/exercises/binary-search-tree/.meta/tests.toml b/exercises/binary-search-tree/.meta/tests.toml
new file mode 100644
index 00000000..0f904baa
--- /dev/null
+++ b/exercises/binary-search-tree/.meta/tests.toml
@@ -0,0 +1,31 @@
+[canonical-tests]
+
+# data is retained
+"e9c93a78-c536-4750-a336-94583d23fafa" = true
+
+# smaller number at left node
+"7a95c9e8-69f6-476a-b0c4-4170cb3f7c91" = true
+
+# same number at left node
+"22b89499-9805-4703-a159-1a6e434c1585" = true
+
+# greater number at right node
+"2e85fdde-77b1-41ed-b6ac-26ce6b663e34" = true
+
+# can create complex tree
+"dd898658-40ab-41d0-965e-7f145bf66e0b" = true
+
+# can sort single number
+"9e0c06ef-aeca-4202-b8e4-97f1ed057d56" = true
+
+# can sort if second number is smaller than first
+"425e6d07-fceb-4681-a4f4-e46920e380bb" = true
+
+# can sort if second number is same as first
+"bd7532cc-6988-4259-bac8-1d50140079ab" = true
+
+# can sort if second number is greater than first
+"b6d1b3a5-9d79-44fd-9013-c83ca92ddd36" = true
+
+# can sort complex tree
+"d00ec9bd-1288-4171-b968-d44d0808c1c8" = true
diff --git a/exercises/binary-search/.meta/tests.toml b/exercises/binary-search/.meta/tests.toml
new file mode 100644
index 00000000..1c35ab1a
--- /dev/null
+++ b/exercises/binary-search/.meta/tests.toml
@@ -0,0 +1,34 @@
+[canonical-tests]
+
+# finds a value in an array with one element
+"b55c24a9-a98d-4379-a08c-2adcf8ebeee8" = true
+
+# finds a value in the middle of an array
+"73469346-b0a0-4011-89bf-989e443d503d" = true
+
+# finds a value at the beginning of an array
+"327bc482-ab85-424e-a724-fb4658e66ddb" = true
+
+# finds a value at the end of an array
+"f9f94b16-fe5e-472c-85ea-c513804c7d59" = true
+
+# finds a value in an array of odd length
+"f0068905-26e3-4342-856d-ad153cadb338" = true
+
+# finds a value in an array of even length
+"fc316b12-c8b3-4f5e-9e89-532b3389de8c" = true
+
+# identifies that a value is not included in the array
+"da7db20a-354f-49f7-a6a1-650a54998aa6" = true
+
+# a value smaller than the array's smallest value is not found
+"95d869ff-3daf-4c79-b622-6e805c675f97" = true
+
+# a value larger than the array's largest value is not found
+"8b24ef45-6e51-4a94-9eac-c2bf38fdb0ba" = true
+
+# nothing is found in an empty array
+"f439a0fa-cf42-4262-8ad1-64bf41ce566a" = true
+
+# nothing is found when the left and right bounds cross
+"2c353967-b56d-40b8-acff-ce43115eed64" = true
diff --git a/exercises/binary/.meta/tests.toml b/exercises/binary/.meta/tests.toml
new file mode 100644
index 00000000..b9de1236
--- /dev/null
+++ b/exercises/binary/.meta/tests.toml
@@ -0,0 +1,46 @@
+[canonical-tests]
+
+# binary 0 is decimal 0
+"567fc71e-1013-4915-9285-bca0648c0844" = true
+
+# binary 1 is decimal 1
+"c0824fb1-6a0a-4e9a-a262-c6c00af99fa8" = true
+
+# binary 10 is decimal 2
+"4d2834fb-3cc3-4159-a8fd-da1098def8ed" = true
+
+# binary 11 is decimal 3
+"b7b2b649-4a7c-4808-9eb9-caf00529bac6" = true
+
+# binary 100 is decimal 4
+"de761aff-73cd-43c1-9e1f-0417f07b1e4a" = true
+
+# binary 1001 is decimal 9
+"7849a8f7-f4a1-4966-963e-503282d6814c" = true
+
+# binary 11010 is decimal 26
+"836a101c-aecb-473b-ba78-962408dcda98" = true
+
+# binary 10001101000 is decimal 1128
+"1c6822a4-8584-438b-8dd4-40f0f0b66371" = true
+
+# binary ignores leading zeros
+"91ffe632-8374-4016-b1d1-d8400d9f940d" = true
+
+# 2 is not a valid binary digit
+"44f7d8b1-ddc3-4751-8be3-700a538b421c" = true
+
+# a number containing a non-binary digit is invalid
+"c263a24d-6870-420f-b783-628feefd7b6e" = true
+
+# a number with trailing non-binary characters is invalid
+"8d81305b-0502-4a07-bfba-051c5526d7f2" = true
+
+# a number with leading non-binary characters is invalid
+"a7f79b6b-039a-4d42-99b4-fcee56679f03" = true
+
+# a number with internal non-binary characters is invalid
+"9e0ece9d-b8aa-46a0-a22b-3bed2e3f741e" = true
+
+# a number and a word whitespace separated is invalid
+"46c8dd65-0c32-4273-bb0d-f2b111bccfbd" = true
diff --git a/exercises/bob/.meta/tests.toml b/exercises/bob/.meta/tests.toml
new file mode 100644
index 00000000..e6ce0825
--- /dev/null
+++ b/exercises/bob/.meta/tests.toml
@@ -0,0 +1,76 @@
+[canonical-tests]
+
+# stating something
+"e162fead-606f-437a-a166-d051915cea8e" = true
+
+# shouting
+"73a966dc-8017-47d6-bb32-cf07d1a5fcd9" = true
+
+# shouting gibberish
+"d6c98afd-df35-4806-b55e-2c457c3ab748" = true
+
+# asking a question
+"8a2e771d-d6f1-4e3f-b6c6-b41495556e37" = true
+
+# asking a numeric question
+"81080c62-4e4d-4066-b30a-48d8d76920d9" = true
+
+# asking gibberish
+"2a02716d-685b-4e2e-a804-2adaf281c01e" = true
+
+# talking forcefully
+"c02f9179-ab16-4aa7-a8dc-940145c385f7" = true
+
+# using acronyms in regular speech
+"153c0e25-9bb5-4ec5-966e-598463658bcd" = true
+
+# forceful question
+"a5193c61-4a92-4f68-93e2-f554eb385ec6" = true
+
+# shouting numbers
+"a20e0c54-2224-4dde-8b10-bd2cdd4f61bc" = true
+
+# no letters
+"f7bc4b92-bdff-421e-a238-ae97f230ccac" = true
+
+# question with no letters
+"bb0011c5-cd52-4a5b-8bfb-a87b6283b0e2" = true
+
+# shouting with special characters
+"496143c8-1c31-4c01-8a08-88427af85c66" = true
+
+# shouting with no exclamation mark
+"e6793c1c-43bd-4b8d-bc11-499aea73925f" = true
+
+# statement containing question mark
+"aa8097cc-c548-4951-8856-14a404dd236a" = true
+
+# non-letters with question
+"9bfc677d-ea3a-45f2-be44-35bc8fa3753e" = true
+
+# prattling on
+"8608c508-f7de-4b17-985b-811878b3cf45" = true
+
+# silence
+"bc39f7c6-f543-41be-9a43-fd1c2f753fc0" = true
+
+# prolonged silence
+"d6c47565-372b-4b09-b1dd-c40552b8378b" = true
+
+# alternate silence
+"4428f28d-4100-4d85-a902-e5a78cb0ecd3" = true
+
+# multiple line question
+"66953780-165b-4e7e-8ce3-4bcb80b6385a" = true
+
+# starting with whitespace
+"5371ef75-d9ea-4103-bcfa-2da973ddec1b" = true
+
+# ending with whitespace
+"05b304d6-f83b-46e7-81e0-4cd3ca647900" = true
+
+# other whitespace
+"72bd5ad3-9b2f-4931-a988-dce1f5771de2" = true
+
+# non-question ending with whitespace
+"12983553-8601-46a8-92fa-fcaa3bc4a2a0" = true
diff --git a/exercises/book-store/.meta/tests.toml b/exercises/book-store/.meta/tests.toml
new file mode 100644
index 00000000..26ea00fd
--- /dev/null
+++ b/exercises/book-store/.meta/tests.toml
@@ -0,0 +1,46 @@
+[canonical-tests]
+
+# Only a single book
+"17146bd5-2e80-4557-ab4c-05632b6b0d01" = true
+
+# Two of the same book
+"cc2de9ac-ff2a-4efd-b7c7-bfe0f43271ce" = true
+
+# Empty basket
+"5a86eac0-45d2-46aa-bbf0-266b94393a1a" = true
+
+# Two different books
+"158bd19a-3db4-4468-ae85-e0638a688990" = true
+
+# Three different books
+"f3833f6b-9332-4a1f-ad98-6c3f8e30e163" = true
+
+# Four different books
+"1951a1db-2fb6-4cd1-a69a-f691b6dd30a2" = true
+
+# Five different books
+"d70f6682-3019-4c3f-aede-83c6a8c647a3" = true
+
+# Two groups of four is cheaper than group of five plus group of three
+"78cacb57-911a-45f1-be52-2a5bd428c634" = true
+
+# Two groups of four is cheaper than groups of five and three
+"f808b5a4-e01f-4c0d-881f-f7b90d9739da" = true
+
+# Group of four plus group of two is cheaper than two groups of three
+"fe96401c-5268-4be2-9d9e-19b76478007c" = true
+
+# Two each of first 4 books and 1 copy each of rest
+"68ea9b78-10ad-420e-a766-836a501d3633" = true
+
+# Two copies of each book
+"c0a779d5-a40c-47ae-9828-a340e936b866" = true
+
+# Three copies of first book and 2 each of remaining
+"18fd86fe-08f1-4b68-969b-392b8af20513" = true
+
+# Three each of first 2 books and 2 each of remaining books
+"0b19a24d-e4cf-4ec8-9db2-8899a41af0da" = true
+
+# Four groups of four are cheaper than two groups each of five and three
+"bb376344-4fb2-49ab-ab85-e38d8354a58d" = true
diff --git a/exercises/bowling/.meta/tests.toml b/exercises/bowling/.meta/tests.toml
new file mode 100644
index 00000000..db5bbd89
--- /dev/null
+++ b/exercises/bowling/.meta/tests.toml
@@ -0,0 +1,91 @@
+[canonical-tests]
+
+# should be able to score a game with all zeros
+"656ae006-25c2-438c-a549-f338e7ec7441" = true
+
+# should be able to score a game with no strikes or spares
+"f85dcc56-cd6b-4875-81b3-e50921e3597b" = true
+
+# a spare followed by zeros is worth ten points
+"d1f56305-3ac2-4fe0-8645-0b37e3073e20" = true
+
+# points scored in the roll after a spare are counted twice
+"0b8c8bb7-764a-4287-801a-f9e9012f8be4" = true
+
+# consecutive spares each get a one roll bonus
+"4d54d502-1565-4691-84cd-f29a09c65bea" = true
+
+# a spare in the last frame gets a one roll bonus that is counted once
+"e5c9cf3d-abbe-4b74-ad48-34051b2b08c0" = true
+
+# a strike earns ten points in a frame with a single roll
+"75269642-2b34-4b72-95a4-9be28ab16902" = true
+
+# points scored in the two rolls after a strike are counted twice as a bonus
+"037f978c-5d01-4e49-bdeb-9e20a2e6f9a6" = true
+
+# consecutive strikes each get the two roll bonus
+"1635e82b-14ec-4cd1-bce4-4ea14bd13a49" = true
+
+# a strike in the last frame gets a two roll bonus that is counted once
+"e483e8b6-cb4b-4959-b310-e3982030d766" = true
+
+# rolling a spare with the two roll bonus does not get a bonus roll
+"9d5c87db-84bc-4e01-8e95-53350c8af1f8" = true
+
+# strikes with the two roll bonus do not get bonus rolls
+"576faac1-7cff-4029-ad72-c16bcada79b5" = true
+
+# a strike with the one roll bonus after a spare in the last frame does not get a bonus
+"72e24404-b6c6-46af-b188-875514c0377b" = true
+
+# all strikes is a perfect game
+"62ee4c72-8ee8-4250-b794-234f1fec17b1" = true
+
+# rolls cannot score negative points
+"1245216b-19c6-422c-b34b-6e4012d7459f" = true
+
+# a roll cannot score more than 10 points
+"5fcbd206-782c-4faa-8f3a-be5c538ba841" = true
+
+# two rolls in a frame cannot score more than 10 points
+"fb023c31-d842-422d-ad7e-79ce1db23c21" = true
+
+# bonus roll after a strike in the last frame cannot score more than 10 points
+"6082d689-d677-4214-80d7-99940189381b" = true
+
+# two bonus rolls after a strike in the last frame cannot score more than 10 points
+"e9565fe6-510a-4675-ba6b-733a56767a45" = true
+
+# two bonus rolls after a strike in the last frame can score more than 10 points if one is a strike
+"2f6acf99-448e-4282-8103-0b9c7df99c3d" = true
+
+# the second bonus rolls after a strike in the last frame cannot be a strike if the first one is not a strike
+"6380495a-8bc4-4cdb-a59f-5f0212dbed01" = true
+
+# second bonus roll after a strike in the last frame cannot score more than 10 points
+"2b2976ea-446c-47a3-9817-42777f09fe7e" = true
+
+# an unstarted game cannot be scored
+"29220245-ac8d-463d-bc19-98a94cfada8a" = true
+
+# an incomplete game cannot be scored
+"4473dc5d-1f86-486f-bf79-426a52ddc955" = true
+
+# cannot roll if game already has ten frames
+"2ccb8980-1b37-4988-b7d1-e5701c317df3" = true
+
+# bonus rolls for a strike in the last frame must be rolled before score can be calculated
+"4864f09b-9df3-4b65-9924-c595ed236f1b" = true
+
+# both bonus rolls for a strike in the last frame must be rolled before score can be calculated
+"537f4e37-4b51-4d1c-97e2-986eb37b2ac1" = true
+
+# bonus roll for a spare in the last frame must be rolled before score can be calculated
+"8134e8c1-4201-4197-bf9f-1431afcde4b9" = true
+
+# cannot roll after bonus roll for spare
+"9d4a9a55-134a-4bad-bae8-3babf84bd570" = true
+
+# cannot roll after bonus rolls for strike
+"d3e02652-a799-4ae3-b53b-68582cc604be" = true
diff --git a/exercises/change/.meta/tests.toml b/exercises/change/.meta/tests.toml
new file mode 100644
index 00000000..72b93b58
--- /dev/null
+++ b/exercises/change/.meta/tests.toml
@@ -0,0 +1,34 @@
+[canonical-tests]
+
+# single coin change
+"36887bea-7f92-4a9c-b0cc-c0e886b3ecc8" = true
+
+# multiple coin change
+"cef21ccc-0811-4e6e-af44-f011e7eab6c6" = true
+
+# change with Lilliputian Coins
+"d60952bc-0c1a-4571-bf0c-41be72690cb3" = true
+
+# change with Lower Elbonia Coins
+"408390b9-fafa-4bb9-b608-ffe6036edb6c" = true
+
+# large target values
+"7421a4cb-1c48-4bf9-99c7-7f049689132f" = true
+
+# possible change without unit coins available
+"f79d2e9b-0ae3-4d6a-bb58-dc978b0dba28" = true
+
+# another possible change without unit coins available
+"9a166411-d35d-4f7f-a007-6724ac266178" = true
+
+# no coins make 0 change
+"bbbcc154-e9e9-4209-a4db-dd6d81ec26bb" = true
+
+# error testing for change smaller than the smallest of coins
+"c8b81d5a-49bd-4b61-af73-8ee5383a2ce1" = true
+
+# error if no combination can add up to target
+"3c43e3e4-63f9-46ac-9476-a67516e98f68" = true
+
+# cannot find negative change values
+"8fe1f076-9b2d-4f44-89fe-8a6ccd63c8f3" = true
diff --git a/exercises/circular-buffer/.meta/tests.toml b/exercises/circular-buffer/.meta/tests.toml
new file mode 100644
index 00000000..9268e709
--- /dev/null
+++ b/exercises/circular-buffer/.meta/tests.toml
@@ -0,0 +1,43 @@
+[canonical-tests]
+
+# reading empty buffer should fail
+"28268ed4-4ff3-45f3-820e-895b44d53dfa" = true
+
+# can read an item just written
+"2e6db04a-58a1-425d-ade8-ac30b5f318f3" = true
+
+# each item may only be read once
+"90741fe8-a448-45ce-be2b-de009a24c144" = true
+
+# items are read in the order they are written
+"be0e62d5-da9c-47a8-b037-5db21827baa7" = true
+
+# full buffer can't be written to
+"2af22046-3e44-4235-bfe6-05ba60439d38" = true
+
+# a read frees up capacity for another write
+"547d192c-bbf0-4369-b8fa-fc37e71f2393" = true
+
+# read position is maintained even across multiple writes
+"04a56659-3a81-4113-816b-6ecb659b4471" = true
+
+# items cleared out of buffer can't be read
+"60c3a19a-81a7-43d7-bb0a-f07242b1111f" = true
+
+# clear frees up capacity for another write
+"45f3ae89-3470-49f3-b50e-362e4b330a59" = true
+
+# clear does nothing on empty buffer
+"e1ac5170-a026-4725-bfbe-0cf332eddecd" = true
+
+# overwrite acts like write on non-full buffer
+"9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b" = true
+
+# overwrite replaces the oldest item on full buffer
+"880f916b-5039-475c-bd5c-83463c36a147" = true
+
+# overwrite replaces the oldest item remaining in buffer following a read
+"bfecab5b-aca1-4fab-a2b0-cd4af2b053c3" = true
+
+# initial clear does not affect wrapping around
+"9cebe63a-c405-437b-8b62-e3fdc1ecec5a" = true
diff --git a/exercises/clock/.meta/tests.toml b/exercises/clock/.meta/tests.toml
new file mode 100644
index 00000000..a37c96d2
--- /dev/null
+++ b/exercises/clock/.meta/tests.toml
@@ -0,0 +1,157 @@
+[canonical-tests]
+
+# on the hour
+"a577bacc-106b-496e-9792-b3083ea8705e" = true
+
+# past the hour
+"b5d0c360-3b88-489b-8e84-68a1c7a4fa23" = true
+
+# midnight is zero hours
+"473223f4-65f3-46ff-a9f7-7663c7e59440" = true
+
+# hour rolls over
+"ca95d24a-5924-447d-9a96-b91c8334725c" = true
+
+# hour rolls over continuously
+"f3826de0-0925-4d69-8ac8-89aea7e52b78" = true
+
+# sixty minutes is next hour
+"a02f7edf-dfd4-4b11-b21a-86de3cc6a95c" = true
+
+# minutes roll over
+"8f520df6-b816-444d-b90f-8a477789beb5" = true
+
+# minutes roll over continuously
+"c75c091b-47ac-4655-8d40-643767fc4eed" = true
+
+# hour and minutes roll over
+"06343ecb-cf39-419d-a3f5-dcbae0cc4c57" = true
+
+# hour and minutes roll over continuously
+"be60810e-f5d9-4b58-9351-a9d1e90e660c" = true
+
+# hour and minutes roll over to exactly midnight
+"1689107b-0b5c-4bea-aad3-65ec9859368a" = true
+
+# negative hour
+"d3088ee8-91b7-4446-9e9d-5e2ad6219d91" = true
+
+# negative hour rolls over
+"77ef6921-f120-4d29-bade-80d54aa43b54" = true
+
+# negative hour rolls over continuously
+"359294b5-972f-4546-bb9a-a85559065234" = true
+
+# negative minutes
+"509db8b7-ac19-47cc-bd3a-a9d2f30b03c0" = true
+
+# negative minutes roll over
+"5d6bb225-130f-4084-84fd-9e0df8996f2a" = true
+
+# negative minutes roll over continuously
+"d483ceef-b520-4f0c-b94a-8d2d58cf0484" = true
+
+# negative sixty minutes is previous hour
+"1cd19447-19c6-44bf-9d04-9f8305ccb9ea" = true
+
+# negative hour and minutes both roll over
+"9d3053aa-4f47-4afc-bd45-d67a72cef4dc" = true
+
+# negative hour and minutes both roll over continuously
+"51d41fcf-491e-4ca0-9cae-2aa4f0163ad4" = true
+
+# add minutes
+"d098e723-ad29-4ef9-997a-2693c4c9d89a" = true
+
+# add no minutes
+"b6ec8f38-e53e-4b22-92a7-60dab1f485f4" = true
+
+# add to next hour
+"efd349dd-0785-453e-9ff8-d7452a8e7269" = true
+
+# add more than one hour
+"749890f7-aba9-4702-acce-87becf4ef9fe" = true
+
+# add more than two hours with carry
+"da63e4c1-1584-46e3-8d18-c9dc802c1713" = true
+
+# add across midnight
+"be167a32-3d33-4cec-a8bc-accd47ddbb71" = true
+
+# add more than one day (1500 min = 25 hrs)
+"6672541e-cdae-46e4-8be7-a820cc3be2a8" = true
+
+# add more than two days
+"1918050d-c79b-4cb7-b707-b607e2745c7e" = true
+
+# subtract minutes
+"37336cac-5ede-43a5-9026-d426cbe40354" = true
+
+# subtract to previous hour
+"0aafa4d0-3b5f-4b12-b3af-e3a9e09c047b" = true
+
+# subtract more than an hour
+"9b4e809c-612f-4b15-aae0-1df0acb801b9" = true
+
+# subtract across midnight
+"8b04bb6a-3d33-4e6c-8de9-f5de6d2c70d6" = true
+
+# subtract more than two hours
+"07c3bbf7-ce4d-4658-86e8-4a77b7a5ccd9" = true
+
+# subtract more than two hours with borrow
+"90ac8a1b-761c-4342-9c9c-cdc3ed5db097" = true
+
+# subtract more than one day (1500 min = 25 hrs)
+"2149f985-7136-44ad-9b29-ec023a97a2b7" = true
+
+# subtract more than two days
+"ba11dbf0-ac27-4acb-ada9-3b853ec08c97" = true
+
+# clocks with same time
+"f2fdad51-499f-4c9b-a791-b28c9282e311" = true
+
+# clocks a minute apart
+"5d409d4b-f862-4960-901e-ec430160b768" = true
+
+# clocks an hour apart
+"a6045fcf-2b52-4a47-8bb2-ef10a064cba5" = true
+
+# clocks with hour overflow
+"66b12758-0be5-448b-a13c-6a44bce83527" = true
+
+# clocks with hour overflow by several days
+"2b19960c-212e-4a71-9aac-c581592f8111" = true
+
+# clocks with negative hour
+"6f8c6541-afac-4a92-b0c2-b10d4e50269f" = true
+
+# clocks with negative hour that wraps
+"bb9d5a68-e324-4bf5-a75e-0e9b1f97a90d" = true
+
+# clocks with negative hour that wraps multiple times
+"56c0326d-565b-4d19-a26f-63b3205778b7" = true
+
+# clocks with minute overflow
+"c90b9de8-ddff-4ffe-9858-da44a40fdbc2" = true
+
+# clocks with minute overflow by several days
+"533a3dc5-59a7-491b-b728-a7a34fe325de" = true
+
+# clocks with negative minute
+"fff49e15-f7b7-4692-a204-0f6052d62636" = true
+
+# clocks with negative minute that wraps
+"605c65bb-21bd-43eb-8f04-878edf508366" = true
+
+# clocks with negative minute that wraps multiple times
+"b87e64ed-212a-4335-91fd-56da8421d077" = true
+
+# clocks with negative hours and minutes
+"822fbf26-1f3b-4b13-b9bf-c914816b53dd" = true
+
+# clocks with negative hours and minutes that wrap
+"e787bccd-cf58-4a1d-841c-ff80eaaccfaa" = true
+
+# full clock and zeroed clock
+"96969ca8-875a-48a1-86ae-257a528c44f5" = true
diff --git a/exercises/collatz-conjecture/.meta/tests.toml b/exercises/collatz-conjecture/.meta/tests.toml
new file mode 100644
index 00000000..83f7c89c
--- /dev/null
+++ b/exercises/collatz-conjecture/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# zero steps for one
+"540a3d51-e7a6-47a5-92a3-4ad1838f0bfd" = true
+
+# divide if even
+"3d76a0a6-ea84-444a-821a-f7857c2c1859" = true
+
+# even and odd steps
+"754dea81-123c-429e-b8bc-db20b05a87b9" = true
+
+# large number of even and odd steps
+"ecfd0210-6f85-44f6-8280-f65534892ff6" = true
+
+# zero is an error
+"7d4750e6-def9-4b86-aec7-9f7eb44f95a3" = true
+
+# negative value is an error
+"c6c795bf-a288-45e9-86a1-841359ad426d" = true
diff --git a/exercises/complex-numbers/.meta/tests.toml b/exercises/complex-numbers/.meta/tests.toml
new file mode 100644
index 00000000..5bd57f42
--- /dev/null
+++ b/exercises/complex-numbers/.meta/tests.toml
@@ -0,0 +1,94 @@
+[canonical-tests]
+
+# Real part of a purely real number
+"9f98e133-eb7f-45b0-9676-cce001cd6f7a" = true
+
+# Real part of a purely imaginary number
+"07988e20-f287-4bb7-90cf-b32c4bffe0f3" = true
+
+# Real part of a number with real and imaginary part
+"4a370e86-939e-43de-a895-a00ca32da60a" = true
+
+# Imaginary part of a purely real number
+"9b3fddef-4c12-4a99-b8f8-e3a42c7ccef6" = true
+
+# Imaginary part of a purely imaginary number
+"a8dafedd-535a-4ed3-8a39-fda103a2b01e" = true
+
+# Imaginary part of a number with real and imaginary part
+"0f998f19-69ee-4c64-80ef-01b086feab80" = true
+
+# Imaginary unit
+"a39b7fd6-6527-492f-8c34-609d2c913879" = true
+
+# Add purely real numbers
+"9a2c8de9-f068-4f6f-b41c-82232cc6c33e" = true
+
+# Add purely imaginary numbers
+"657c55e1-b14b-4ba7-bd5c-19db22b7d659" = true
+
+# Add numbers with real and imaginary part
+"4e1395f5-572b-4ce8-bfa9-9a63056888da" = true
+
+# Subtract purely real numbers
+"1155dc45-e4f7-44b8-af34-a91aa431475d" = true
+
+# Subtract purely imaginary numbers
+"f95e9da8-acd5-4da4-ac7c-c861b02f774b" = true
+
+# Subtract numbers with real and imaginary part
+"f876feb1-f9d1-4d34-b067-b599a8746400" = true
+
+# Multiply purely real numbers
+"8a0366c0-9e16-431f-9fd7-40ac46ff4ec4" = true
+
+# Multiply purely imaginary numbers
+"e560ed2b-0b80-4b4f-90f2-63cefc911aaf" = true
+
+# Multiply numbers with real and imaginary part
+"4d1d10f0-f8d4-48a0-b1d0-f284ada567e6" = true
+
+# Divide purely real numbers
+"b0571ddb-9045-412b-9c15-cd1d816d36c1" = true
+
+# Divide purely imaginary numbers
+"5bb4c7e4-9934-4237-93cc-5780764fdbdd" = true
+
+# Divide numbers with real and imaginary part
+"c4e7fef5-64ac-4537-91c2-c6529707701f" = true
+
+# Absolute value of a positive purely real number
+"c56a7332-aad2-4437-83a0-b3580ecee843" = true
+
+# Absolute value of a negative purely real number
+"cf88d7d3-ee74-4f4e-8a88-a1b0090ecb0c" = true
+
+# Absolute value of a purely imaginary number with positive imaginary part
+"bbe26568-86c1-4bb4-ba7a-da5697e2b994" = true
+
+# Absolute value of a purely imaginary number with negative imaginary part
+"3b48233d-468e-4276-9f59-70f4ca1f26f3" = true
+
+# Absolute value of a number with real and imaginary part
+"fe400a9f-aa22-4b49-af92-51e0f5a2a6d3" = true
+
+# Conjugate a purely real number
+"fb2d0792-e55a-4484-9443-df1eddfc84a2" = true
+
+# Conjugate a purely imaginary number
+"e37fe7ac-a968-4694-a460-66cb605f8691" = true
+
+# Conjugate a number with real and imaginary part
+"f7704498-d0be-4192-aaf5-a1f3a7f43e68" = true
+
+# Euler's identity/formula
+"6d96d4c6-2edb-445b-94a2-7de6d4caaf60" = true
+
+# Exponential of 0
+"2d2c05a0-4038-4427-a24d-72f6624aa45f" = true
+
+# Exponential of a purely real number
+"ed87f1bd-b187-45d6-8ece-7e331232c809" = true
+
+# Exponential of a number with real and imaginary part
+"08eedacc-5a95-44fc-8789-1547b27a8702" = true
diff --git a/exercises/connect/.meta/tests.toml b/exercises/connect/.meta/tests.toml
new file mode 100644
index 00000000..73c71767
--- /dev/null
+++ b/exercises/connect/.meta/tests.toml
@@ -0,0 +1,31 @@
+[canonical-tests]
+
+# an empty board has no winner
+"6eff0df4-3e92-478d-9b54-d3e8b354db56" = true
+
+# X can win on a 1x1 board
+"298b94c0-b46d-45d8-b34b-0fa2ea71f0a4" = true
+
+# O can win on a 1x1 board
+"763bbae0-cb8f-4f28-bc21-5be16a5722dc" = true
+
+# only edges does not make a winner
+"819fde60-9ae2-485e-a024-cbb8ea68751b" = true
+
+# illegal diagonal does not make a winner
+"2c56a0d5-9528-41e5-b92b-499dfe08506c" = true
+
+# nobody wins crossing adjacent angles
+"41cce3ef-43ca-4963-970a-c05d39aa1cc1" = true
+
+# X wins crossing from left to right
+"cd61c143-92f6-4a8d-84d9-cb2b359e226b" = true
+
+# O wins crossing from top to bottom
+"73d1eda6-16ab-4460-9904-b5f5dd401d0b" = true
+
+# X wins using a convoluted path
+"c3a2a550-944a-4637-8b3f-1e1bf1340a3d" = true
+
+# X wins using a spiral path
+"17e76fa8-f731-4db7-92ad-ed2a285d31f3" = true
diff --git a/exercises/crypto-square/.meta/tests.toml b/exercises/crypto-square/.meta/tests.toml
new file mode 100644
index 00000000..84691f12
--- /dev/null
+++ b/exercises/crypto-square/.meta/tests.toml
@@ -0,0 +1,22 @@
+[canonical-tests]
+
+# empty plaintext results in an empty ciphertext
+"407c3837-9aa7-4111-ab63-ec54b58e8e9f" = true
+
+# Lowercase
+"64131d65-6fd9-4f58-bdd8-4a2370fb481d" = true
+
+# Remove spaces
+"63a4b0ed-1e3c-41ea-a999-f6f26ba447d6" = true
+
+# Remove punctuation
+"1b5348a1-7893-44c1-8197-42d48d18756c" = true
+
+# 9 character plaintext results in 3 chunks of 3 characters
+"8574a1d3-4a08-4cec-a7c7-de93a164f41a" = true
+
+# 8 character plaintext results in 3 chunks, the last one with a trailing space
+"a65d3fa1-9e09-43f9-bcec-7a672aec3eae" = true
+
+# 54 character plaintext results in 7 chunks, the last two with trailing spaces
+"fbcb0c6d-4c39-4a31-83f6-c473baa6af80" = true
diff --git a/exercises/custom-set/.meta/tests.toml b/exercises/custom-set/.meta/tests.toml
new file mode 100644
index 00000000..b906845c
--- /dev/null
+++ b/exercises/custom-set/.meta/tests.toml
@@ -0,0 +1,115 @@
+[canonical-tests]
+
+# sets with no elements are empty
+"20c5f855-f83a-44a7-abdd-fe75c6cf022b" = true
+
+# sets with elements are not empty
+"d506485d-5706-40db-b7d8-5ceb5acf88d2" = true
+
+# nothing is contained in an empty set
+"759b9740-3417-44c3-8ca3-262b3c281043" = true
+
+# when the element is in the set
+"f83cd2d1-2a85-41bc-b6be-80adbff4be49" = true
+
+# when the element is not in the set
+"93423fc0-44d0-4bc0-a2ac-376de8d7af34" = true
+
+# empty set is a subset of another empty set
+"c392923a-637b-4495-b28e-34742cd6157a" = true
+
+# empty set is a subset of non-empty set
+"5635b113-be8c-4c6f-b9a9-23c485193917" = true
+
+# non-empty set is not a subset of empty set
+"832eda58-6d6e-44e2-92c2-be8cf0173cee" = true
+
+# set is a subset of set with exact same elements
+"c830c578-8f97-4036-b082-89feda876131" = true
+
+# set is a subset of larger set with same elements
+"476a4a1c-0fd1-430f-aa65-5b70cbc810c5" = true
+
+# set is not a subset of set that does not contain its elements
+"d2498999-3e46-48e4-9660-1e20c3329d3d" = true
+
+# the empty set is disjoint with itself
+"7d38155e-f472-4a7e-9ad8-5c1f8f95e4cc" = true
+
+# empty set is disjoint with non-empty set
+"7a2b3938-64b6-4b32-901a-fe16891998a6" = true
+
+# non-empty set is disjoint with empty set
+"589574a0-8b48-48ea-88b0-b652c5fe476f" = true
+
+# sets are not disjoint if they share an element
+"febeaf4f-f180-4499-91fa-59165955a523" = true
+
+# sets are disjoint if they share no elements
+"0de20d2f-c952-468a-88c8-5e056740f020" = true
+
+# empty sets are equal
+"4bd24adb-45da-4320-9ff6-38c044e9dff8" = true
+
+# empty set is not equal to non-empty set
+"f65c0a0e-6632-4b2d-b82c-b7c6da2ec224" = true
+
+# non-empty set is not equal to empty set
+"81e53307-7683-4b1e-a30c-7e49155fe3ca" = true
+
+# sets with the same elements are equal
+"d57c5d7c-a7f3-48cc-a162-6b488c0fbbd0" = true
+
+# sets with different elements are not equal
+"dd61bafc-6653-42cc-961a-ab071ee0ee85" = true
+
+# set is not equal to larger set with same elements
+"06059caf-9bf4-425e-aaff-88966cb3ea14" = true
+
+# add to empty set
+"8a677c3c-a658-4d39-bb88-5b5b1a9659f4" = true
+
+# add to non-empty set
+"0903dd45-904d-4cf2-bddd-0905e1a8d125" = true
+
+# adding an existing element does not change the set
+"b0eb7bb7-5e5d-4733-b582-af771476cb99" = true
+
+# intersection of two empty sets is an empty set
+"893d5333-33b8-4151-a3d4-8f273358208a" = true
+
+# intersection of an empty set and non-empty set is an empty set
+"d739940e-def2-41ab-a7bb-aaf60f7d782c" = true
+
+# intersection of a non-empty set and an empty set is an empty set
+"3607d9d8-c895-4d6f-ac16-a14956e0a4b7" = true
+
+# intersection of two sets with no shared elements is an empty set
+"b5120abf-5b5e-41ab-aede-4de2ad85c34e" = true
+
+# intersection of two sets with shared elements is a set of the shared elements
+"af21ca1b-fac9-499c-81c0-92a591653d49" = true
+
+# difference of two empty sets is an empty set
+"c5e6e2e4-50e9-4bc2-b89f-c518f015b57e" = true
+
+# difference of empty set and non-empty set is an empty set
+"2024cc92-5c26-44ed-aafd-e6ca27d6fcd2" = true
+
+# difference of a non-empty set and an empty set is the non-empty set
+"e79edee7-08aa-4c19-9382-f6820974b43e" = true
+
+# difference of two non-empty sets is a set of elements that are only in the first set
+"c5ac673e-d707-4db5-8d69-7082c3a5437e" = true
+
+# union of empty sets is an empty set
+"c45aed16-5494-455a-9033-5d4c93589dc6" = true
+
+# union of an empty set and non-empty set is the non-empty set
+"9d258545-33c2-4fcb-a340-9f8aa69e7a41" = true
+
+# union of a non-empty set and empty set is the non-empty set
+"3aade50c-80c7-4db8-853d-75bac5818b83" = true
+
+# union of non-empty sets contains all unique elements
+"a00bb91f-c4b4-4844-8f77-c73e2e9df77c" = true
diff --git a/exercises/darts/.meta/tests.toml b/exercises/darts/.meta/tests.toml
new file mode 100644
index 00000000..efa28c7d
--- /dev/null
+++ b/exercises/darts/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# Missed target
+"9033f731-0a3a-4d9c-b1c0-34a1c8362afb" = true
+
+# On the outer circle
+"4c9f6ff4-c489-45fd-be8a-1fcb08b4d0ba" = true
+
+# On the middle circle
+"14378687-ee58-4c9b-a323-b089d5274be8" = true
+
+# On the inner circle
+"849e2e63-85bd-4fed-bc3b-781ae962e2c9" = true
+
+# Exactly on centre
+"1c5ffd9f-ea66-462f-9f06-a1303de5a226" = true
+
+# Near the centre
+"b65abce3-a679-4550-8115-4b74bda06088" = true
+
+# Just within the inner circle
+"66c29c1d-44f5-40cf-9927-e09a1305b399" = true
+
+# Just outside the inner circle
+"d1012f63-c97c-4394-b944-7beb3d0b141a" = true
+
+# Just within the middle circle
+"ab2b5666-b0b4-49c3-9b27-205e790ed945" = true
+
+# Just outside the middle circle
+"70f1424e-d690-4860-8caf-9740a52c0161" = true
+
+# Just within the outer circle
+"a7dbf8db-419c-4712-8a7f-67602b69b293" = true
+
+# Just outside the outer circle
+"e0f39315-9f9a-4546-96e4-a9475b885aa7" = true
+
+# Asymmetric position between the inner and middle circles
+"045d7d18-d863-4229-818e-b50828c75d19" = true
diff --git a/exercises/diamond/.meta/tests.toml b/exercises/diamond/.meta/tests.toml
new file mode 100644
index 00000000..3c121906
--- /dev/null
+++ b/exercises/diamond/.meta/tests.toml
@@ -0,0 +1,16 @@
+[canonical-tests]
+
+# Degenerate case with a single 'A' row
+"202fb4cc-6a38-4883-9193-a29d5cb92076" = true
+
+# Degenerate case with no row containing 3 distinct groups of spaces
+"bd6a6d78-9302-42e9-8f60-ac1461e9abae" = true
+
+# Smallest non-degenerate case with odd diamond side length
+"af8efb49-14ed-447f-8944-4cc59ce3fd76" = true
+
+# Smallest non-degenerate case with even diamond side length
+"e0c19a95-9888-4d05-86a0-fa81b9e70d1d" = true
+
+# Largest possible diamond
+"82ea9aa9-4c0e-442a-b07e-40204e925944" = true
diff --git a/exercises/difference-of-squares/.meta/tests.toml b/exercises/difference-of-squares/.meta/tests.toml
new file mode 100644
index 00000000..3eaac74f
--- /dev/null
+++ b/exercises/difference-of-squares/.meta/tests.toml
@@ -0,0 +1,28 @@
+[canonical-tests]
+
+# square of sum 1
+"e46c542b-31fc-4506-bcae-6b62b3268537" = true
+
+# square of sum 5
+"9b3f96cb-638d-41ee-99b7-b4f9c0622948" = true
+
+# square of sum 100
+"54ba043f-3c35-4d43-86ff-3a41625d5e86" = true
+
+# sum of squares 1
+"01d84507-b03e-4238-9395-dd61d03074b5" = true
+
+# sum of squares 5
+"c93900cd-8cc2-4ca4-917b-dd3027023499" = true
+
+# sum of squares 100
+"94807386-73e4-4d9e-8dec-69eb135b19e4" = true
+
+# difference of squares 1
+"44f72ae6-31a7-437f-858d-2c0837adabb6" = true
+
+# difference of squares 5
+"005cb2bf-a0c8-46f3-ae25-924029f8b00b" = true
+
+# difference of squares 100
+"b1bf19de-9a16-41c0-a62b-1f02ecc0b036" = true
diff --git a/exercises/diffie-hellman/.meta/tests.toml b/exercises/diffie-hellman/.meta/tests.toml
new file mode 100644
index 00000000..c859d532
--- /dev/null
+++ b/exercises/diffie-hellman/.meta/tests.toml
@@ -0,0 +1,16 @@
+[canonical-tests]
+
+# private key is in range 1 .. p
+"1b97bf38-4307-418e-bfd2-446ffc77588d" = true
+
+# private key is random
+"68b2a5f7-7755-44c3-97b2-d28d21f014a9" = true
+
+# can calculate public key using private key
+"b4161d8e-53a1-4241-ae8f-48cc86527f22" = true
+
+# can calculate secret using other party's public key
+"cd02ad45-3f52-4510-99cc-5161dad948a8" = true
+
+# key exchange
+"17f13c61-a111-4075-9a1f-c2d4636dfa60" = true
diff --git a/exercises/dnd-character/.meta/tests.toml b/exercises/dnd-character/.meta/tests.toml
new file mode 100644
index 00000000..5b1242fd
--- /dev/null
+++ b/exercises/dnd-character/.meta/tests.toml
@@ -0,0 +1,58 @@
+[canonical-tests]
+
+# ability modifier for score 3 is -4
+"1e9ae1dc-35bd-43ba-aa08-e4b94c20fa37" = true
+
+# ability modifier for score 4 is -3
+"cc9bb24e-56b8-4e9e-989d-a0d1a29ebb9c" = true
+
+# ability modifier for score 5 is -3
+"5b519fcd-6946-41ee-91fe-34b4f9808326" = true
+
+# ability modifier for score 6 is -2
+"dc2913bd-6d7a-402e-b1e2-6d568b1cbe21" = true
+
+# ability modifier for score 7 is -2
+"099440f5-0d66-4b1a-8a10-8f3a03cc499f" = true
+
+# ability modifier for score 8 is -1
+"cfda6e5c-3489-42f0-b22b-4acb47084df0" = true
+
+# ability modifier for score 9 is -1
+"c70f0507-fa7e-4228-8463-858bfbba1754" = true
+
+# ability modifier for score 10 is 0
+"6f4e6c88-1cd9-46a0-92b8-db4a99b372f7" = true
+
+# ability modifier for score 11 is 0
+"e00d9e5c-63c8-413f-879d-cd9be9697097" = true
+
+# ability modifier for score 12 is +1
+"eea06f3c-8de0-45e7-9d9d-b8cab4179715" = true
+
+# ability modifier for score 13 is +1
+"9c51f6be-db72-4af7-92ac-b293a02c0dcd" = true
+
+# ability modifier for score 14 is +2
+"94053a5d-53b6-4efc-b669-a8b5098f7762" = true
+
+# ability modifier for score 15 is +2
+"8c33e7ca-3f9f-4820-8ab3-65f2c9e2f0e2" = true
+
+# ability modifier for score 16 is +3
+"c3ec871e-1791-44d0-b3cc-77e5fb4cd33d" = true
+
+# ability modifier for score 17 is +3
+"3d053cee-2888-4616-b9fd-602a3b1efff4" = true
+
+# ability modifier for score 18 is +4
+"bafd997a-e852-4e56-9f65-14b60261faee" = true
+
+# random ability is within range
+"4f28f19c-2e47-4453-a46a-c0d365259c14" = true
+
+# random character is valid
+"385d7e72-864f-4e88-8279-81a7d75b04ad" = true
+
+# each ability is only calculated once
+"2ca77b9b-c099-46c3-a02c-0d0f68ffa0fe" = true
diff --git a/exercises/dominoes/.meta/tests.toml b/exercises/dominoes/.meta/tests.toml
new file mode 100644
index 00000000..40af88d3
--- /dev/null
+++ b/exercises/dominoes/.meta/tests.toml
@@ -0,0 +1,37 @@
+[canonical-tests]
+
+# empty input = empty output
+"31a673f2-5e54-49fe-bd79-1c1dae476c9c" = true
+
+# singleton input = singleton output
+"4f99b933-367b-404b-8c6d-36d5923ee476" = true
+
+# singleton that can't be chained
+"91122d10-5ec7-47cb-b759-033756375869" = true
+
+# three elements
+"be8bc26b-fd3d-440b-8e9f-d698a0623be3" = true
+
+# can reverse dominoes
+"99e615c6-c059-401c-9e87-ad7af11fea5c" = true
+
+# can't be chained
+"51f0c291-5d43-40c5-b316-0429069528c9" = true
+
+# disconnected - simple
+"9a75e078-a025-4c23-8c3a-238553657f39" = true
+
+# disconnected - double loop
+"0da0c7fe-d492-445d-b9ef-1f111f07a301" = true
+
+# disconnected - single isolated
+"b6087ff0-f555-4ea0-a71c-f9d707c5994a" = true
+
+# need backtrack
+"2174fbdc-8b48-4bac-9914-8090d06ef978" = true
+
+# separate loops
+"167bb480-dfd1-4318-a20d-4f90adb4a09f" = true
+
+# nine elements
+"cd061538-6046-45a7-ace9-6708fe8f6504" = true
diff --git a/exercises/etl/.meta/tests.toml b/exercises/etl/.meta/tests.toml
new file mode 100644
index 00000000..83821984
--- /dev/null
+++ b/exercises/etl/.meta/tests.toml
@@ -0,0 +1,13 @@
+[canonical-tests]
+
+# single letter
+"78a7a9f9-4490-4a47-8ee9-5a38bb47d28f" = true
+
+# single score with multiple letters
+"60dbd000-451d-44c7-bdbb-97c73ac1f497" = true
+
+# multiple scores with multiple letters
+"f5c5de0c-301f-4fdd-a0e5-df97d4214f54" = true
+
+# multiple scores with differing numbers of letters
+"5db8ea89-ecb4-4dcd-902f-2b418cc87b9d" = true
diff --git a/exercises/flatten-array/.meta/tests.toml b/exercises/flatten-array/.meta/tests.toml
new file mode 100644
index 00000000..7c2d72db
--- /dev/null
+++ b/exercises/flatten-array/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# no nesting
+"d268b919-963c-442d-9f07-82b93f1b518c" = true
+
+# flattens array with just integers present
+"c84440cc-bb3a-48a6-862c-94cf23f2815d" = true
+
+# 5 level nesting
+"d3d99d39-6be5-44f5-a31d-6037d92ba34f" = true
+
+# 6 level nesting
+"d572bdba-c127-43ed-bdcd-6222ac83d9f7" = true
+
+# 6 level nest list with null values
+"ef1d4790-1b1e-4939-a179-51ace0829dbd" = true
+
+# all values in nested list are null
+"85721643-705a-4150-93ab-7ae398e2942d" = true
diff --git a/exercises/food-chain/.meta/tests.toml b/exercises/food-chain/.meta/tests.toml
new file mode 100644
index 00000000..6094c2cc
--- /dev/null
+++ b/exercises/food-chain/.meta/tests.toml
@@ -0,0 +1,31 @@
+[canonical-tests]
+
+# fly
+"751dce68-9412-496e-b6e8-855998c56166" = true
+
+# spider
+"6c56f861-0c5e-4907-9a9d-b2efae389379" = true
+
+# bird
+"3edf5f33-bef1-4e39-ae67-ca5eb79203fa" = true
+
+# cat
+"e866a758-e1ff-400e-9f35-f27f28cc288f" = true
+
+# dog
+"3f02c30e-496b-4b2a-8491-bc7e2953cafb" = true
+
+# goat
+"4b3fd221-01ea-46e0-825b-5734634fbc59" = true
+
+# cow
+"1b707da9-7001-4fac-941f-22ad9c7a65d4" = true
+
+# horse
+"3cb10d46-ae4e-4d2c-9296-83c9ffc04cdc" = true
+
+# multiple verses
+"22b863d5-17e4-4d1e-93e4-617329a5c050" = true
+
+# full song
+"e626b32b-745c-4101-bcbd-3b13456893db" = true
diff --git a/exercises/forth/.meta/tests.toml b/exercises/forth/.meta/tests.toml
new file mode 100644
index 00000000..68d39fc6
--- /dev/null
+++ b/exercises/forth/.meta/tests.toml
@@ -0,0 +1,139 @@
+[canonical-tests]
+
+# numbers just get pushed onto the stack
+"9962203f-f00a-4a85-b404-8a8ecbcec09d" = true
+
+# can add two numbers
+"9e69588e-a3d8-41a3-a371-ea02206c1e6e" = true
+
+# errors if there is nothing on the stack
+"52336dd3-30da-4e5c-8523-bdf9a3427657" = true
+
+# errors if there is only one value on the stack
+"06efb9a4-817a-435e-b509-06166993c1b8" = true
+
+# can subtract two numbers
+"09687c99-7bbc-44af-8526-e402f997ccbf" = true
+
+# errors if there is nothing on the stack
+"5d63eee2-1f7d-4538-b475-e27682ab8032" = true
+
+# errors if there is only one value on the stack
+"b3cee1b2-9159-418a-b00d-a1bb3765c23b" = true
+
+# can multiply two numbers
+"5df0ceb5-922e-401f-974d-8287427dbf21" = true
+
+# errors if there is nothing on the stack
+"9e004339-15ac-4063-8ec1-5720f4e75046" = true
+
+# errors if there is only one value on the stack
+"8ba4b432-9f94-41e0-8fae-3b3712bd51b3" = true
+
+# can divide two numbers
+"e74c2204-b057-4cff-9aa9-31c7c97a93f5" = true
+
+# performs integer division
+"54f6711c-4b14-4bb0-98ad-d974a22c4620" = true
+
+# errors if dividing by zero
+"a5df3219-29b4-4d2f-b427-81f82f42a3f1" = true
+
+# errors if there is nothing on the stack
+"1d5bb6b3-6749-4e02-8a79-b5d4d334cb8a" = true
+
+# errors if there is only one value on the stack
+"d5547f43-c2ff-4d5c-9cb0-2a4f6684c20d" = true
+
+# addition and subtraction
+"ee28d729-6692-4a30-b9be-0d830c52a68c" = true
+
+# multiplication and division
+"40b197da-fa4b-4aca-a50b-f000d19422c1" = true
+
+# copies a value on the stack
+"c5758235-6eef-4bf6-ab62-c878e50b9957" = true
+
+# copies the top value on the stack
+"f6889006-5a40-41e7-beb3-43b09e5a22f4" = true
+
+# errors if there is nothing on the stack
+"40b7569c-8401-4bd4-a30d-9adf70d11bc4" = true
+
+# removes the top value on the stack if it is the only one
+"1971da68-1df2-4569-927a-72bf5bb7263c" = true
+
+# removes the top value on the stack if it is not the only one
+"8929d9f2-4a78-4e0f-90ad-be1a0f313fd9" = true
+
+# errors if there is nothing on the stack
+"6dd31873-6dd7-4cb8-9e90-7daa33ba045c" = true
+
+# swaps the top two values on the stack if they are the only ones
+"3ee68e62-f98a-4cce-9e6c-8aae6c65a4e3" = true
+
+# swaps the top two values on the stack if they are not the only ones
+"8ce869d5-a503-44e4-ab55-1da36816ff1c" = true
+
+# errors if there is nothing on the stack
+"74ba5b2a-b028-4759-9176-c5c0e7b2b154" = true
+
+# errors if there is only one value on the stack
+"dd52e154-5d0d-4a5c-9e5d-73eb36052bc8" = true
+
+# copies the second element if there are only two
+"a2654074-ba68-4f93-b014-6b12693a8b50" = true
+
+# copies the second element if there are more than two
+"c5b51097-741a-4da7-8736-5c93fa856339" = true
+
+# errors if there is nothing on the stack
+"6e1703a6-5963-4a03-abba-02e77e3181fd" = true
+
+# errors if there is only one value on the stack
+"ee574dc4-ef71-46f6-8c6a-b4af3a10c45f" = true
+
+# can consist of built-in words
+"ed45cbbf-4dbf-4901-825b-54b20dbee53b" = true
+
+# execute in the right order
+"2726ea44-73e4-436b-bc2b-5ff0c6aa014b" = true
+
+# can override other user-defined words
+"9e53c2d0-b8ef-4ad8-b2c9-a559b421eb33" = true
+
+# can override built-in words
+"669db3f3-5bd6-4be0-83d1-618cd6e4984b" = true
+
+# can override built-in operators
+"588de2f0-c56e-4c68-be0b-0bb1e603c500" = true
+
+# can use different words with the same name
+"ac12aaaf-26c6-4a10-8b3c-1c958fa2914c" = true
+
+# can define word that uses word with the same name
+"53f82ef0-2750-4ccb-ac04-5d8c1aefabb1" = true
+
+# cannot redefine numbers
+"35958cee-a976-4a0f-9378-f678518fa322" = true
+
+# errors if executing a non-existent word
+"5180f261-89dd-491e-b230-62737e09806f" = true
+
+# DUP is case-insensitive
+"7b83bb2e-b0e8-461f-ad3b-96ee2e111ed6" = true
+
+# DROP is case-insensitive
+"339ed30b-f5b4-47ff-ab1c-67591a9cd336" = true
+
+# SWAP is case-insensitive
+"ee1af31e-1355-4b1b-bb95-f9d0b2961b87" = true
+
+# OVER is case-insensitive
+"acdc3a49-14c8-4cc2-945d-11edee6408fa" = true
+
+# user-defined words are case-insensitive
+"5934454f-a24f-4efc-9fdd-5794e5f0c23c" = true
+
+# definitions are case-insensitive
+"037d4299-195f-4be7-a46d-f07ca6280a06" = true
diff --git a/exercises/gigasecond/.meta/tests.toml b/exercises/gigasecond/.meta/tests.toml
new file mode 100644
index 00000000..53074f55
--- /dev/null
+++ b/exercises/gigasecond/.meta/tests.toml
@@ -0,0 +1,16 @@
+[canonical-tests]
+
+# date only specification of time
+"92fbe71c-ea52-4fac-bd77-be38023cacf7" = true
+
+# second test for date only specification of time
+"6d86dd16-6f7a-47be-9e58-bb9fb2ae1433" = true
+
+# third test for date only specification of time
+"77eb8502-2bca-4d92-89d9-7b39ace28dd5" = true
+
+# full time specified
+"c9d89a7d-06f8-4e28-a305-64f1b2abc693" = true
+
+# full time with day roll-over
+"09d4e30e-728a-4b52-9005-be44a58d9eba" = true
diff --git a/exercises/go-counting/.meta/tests.toml b/exercises/go-counting/.meta/tests.toml
new file mode 100644
index 00000000..cb45d53e
--- /dev/null
+++ b/exercises/go-counting/.meta/tests.toml
@@ -0,0 +1,34 @@
+[canonical-tests]
+
+# Black corner territory on 5x5 board
+"94d0c01a-17d0-424c-aab5-2736d0da3939" = true
+
+# White center territory on 5x5 board
+"b33bec54-356a-485c-9c71-1142a9403213" = true
+
+# Open corner territory on 5x5 board
+"def7d124-422e-44ae-90e5-ceda09399bda" = true
+
+# A stone and not a territory on 5x5 board
+"57d79036-2618-47f4-aa87-56c06d362e3d" = true
+
+# Invalid because X is too low for 5x5 board
+"0c84f852-e032-4762-9010-99f6a001da96" = true
+
+# Invalid because X is too high for 5x5 board
+"6f867945-9b2c-4bdd-b23e-b55fe2069a68" = true
+
+# Invalid because Y is too low for 5x5 board
+"d67aaffd-fdf1-4e7f-b9e9-79897402b64a" = true
+
+# Invalid because Y is too high for 5x5 board
+"14f23c25-799e-4371-b3e5-777a2c30357a" = true
+
+# One territory is the whole board
+"37fb04b5-98c1-4b96-8c16-af2d13624afd" = true
+
+# Two territory rectangular board
+"9a1c59b7-234b-495a-8d60-638489f0fc0a" = true
+
+# Two region rectangular board
+"d1645953-1cd5-4221-af6f-8164f96249e1" = true
diff --git a/exercises/grade-school/.meta/tests.toml b/exercises/grade-school/.meta/tests.toml
new file mode 100644
index 00000000..bdbeb158
--- /dev/null
+++ b/exercises/grade-school/.meta/tests.toml
@@ -0,0 +1,22 @@
+[canonical-tests]
+
+# Adding a student adds them to the sorted roster
+"6d0a30e4-1b4e-472e-8e20-c41702125667" = true
+
+# Adding more student adds them to the sorted roster
+"233be705-dd58-4968-889d-fb3c7954c9cc" = true
+
+# Adding students to different grades adds them to the same sorted roster
+"75a51579-d1d7-407c-a2f8-2166e984e8ab" = true
+
+# Roster returns an empty list if there are no students enrolled
+"a3f0fb58-f240-4723-8ddc-e644666b85cc" = true
+
+# Student names with grades are displayed in the same sorted roster
+"180a8ff9-5b94-43fc-9db1-d46b4a8c93b6" = true
+
+# Grade returns the students in that grade in alphabetical order
+"1bfbcef1-e4a3-49e8-8d22-f6f9f386187e" = true
+
+# Grade returns an empty list if there are no students in that grade
+"5e67aa3c-a3c6-4407-a183-d8fe59cd1630" = true
diff --git a/exercises/grep/.meta/tests.toml b/exercises/grep/.meta/tests.toml
new file mode 100644
index 00000000..b8010568
--- /dev/null
+++ b/exercises/grep/.meta/tests.toml
@@ -0,0 +1,76 @@
+[canonical-tests]
+
+# One file, one match, no flags
+"9049fdfd-53a7-4480-a390-375203837d09" = true
+
+# One file, one match, print line numbers flag
+"76519cce-98e3-46cd-b287-aac31b1d77d6" = true
+
+# One file, one match, case-insensitive flag
+"af0b6d3c-e0e8-475e-a112-c0fc10a1eb30" = true
+
+# One file, one match, print file names flag
+"ff7af839-d1b8-4856-a53e-99283579b672" = true
+
+# One file, one match, match entire lines flag
+"8625238a-720c-4a16-81f2-924ec8e222cb" = true
+
+# One file, one match, multiple flags
+"2a6266b3-a60f-475c-a5f5-f5008a717d3e" = true
+
+# One file, several matches, no flags
+"842222da-32e8-4646-89df-0d38220f77a1" = true
+
+# One file, several matches, print line numbers flag
+"4d84f45f-a1d8-4c2e-a00e-0b292233828c" = true
+
+# One file, several matches, match entire lines flag
+"0a483b66-315b-45f5-bc85-3ce353a22539" = true
+
+# One file, several matches, case-insensitive flag
+"3d2ca86a-edd7-494c-8938-8eeed1c61cfa" = true
+
+# One file, several matches, inverted flag
+"1f52001f-f224-4521-9456-11120cad4432" = true
+
+# One file, no matches, various flags
+"7a6ede7f-7dd5-4364-8bf8-0697c53a09fe" = true
+
+# One file, one match, file flag takes precedence over line flag
+"3d3dfc23-8f2a-4e34-abd6-7b7d140291dc" = true
+
+# One file, several matches, inverted and match entire lines flags
+"87b21b24-b788-4d6e-a68b-7afe9ca141fe" = true
+
+# Multiple files, one match, no flags
+"ba496a23-6149-41c6-a027-28064ed533e5" = true
+
+# Multiple files, several matches, no flags
+"4539bd36-6daa-4bc3-8e45-051f69f5aa95" = true
+
+# Multiple files, several matches, print line numbers flag
+"9fb4cc67-78e2-4761-8e6b-a4b57aba1938" = true
+
+# Multiple files, one match, print file names flag
+"aeee1ef3-93c7-4cd5-af10-876f8c9ccc73" = true
+
+# Multiple files, several matches, case-insensitive flag
+"d69f3606-7d15-4ddf-89ae-01df198e6b6c" = true
+
+# Multiple files, several matches, inverted flag
+"82ef739d-6701-4086-b911-007d1a3deb21" = true
+
+# Multiple files, one match, match entire lines flag
+"77b2eb07-2921-4ea0-8971-7636b44f5d29" = true
+
+# Multiple files, one match, multiple flags
+"e53a2842-55bb-4078-9bb5-04ac38929989" = true
+
+# Multiple files, no matches, various flags
+"9c4f7f9a-a555-4e32-bb06-4b8f8869b2cb" = true
+
+# Multiple files, several matches, file flag takes precedence over line number flag
+"ba5a540d-bffd-481b-bd0c-d9a30f225e01" = true
+
+# Multiple files, several matches, inverted and match entire lines flags
+"ff406330-2f0b-4b17-9ee4-4b71c31dd6d2" = true
diff --git a/exercises/hamming/.meta/tests.toml b/exercises/hamming/.meta/tests.toml
new file mode 100644
index 00000000..f261259b
--- /dev/null
+++ b/exercises/hamming/.meta/tests.toml
@@ -0,0 +1,28 @@
+[canonical-tests]
+
+# empty strands
+"f6dcb64f-03b0-4b60-81b1-3c9dbf47e887" = true
+
+# single letter identical strands
+"54681314-eee2-439a-9db0-b0636c656156" = true
+
+# single letter different strands
+"294479a3-a4c8-478f-8d63-6209815a827b" = true
+
+# long identical strands
+"9aed5f34-5693-4344-9b31-40c692fb5592" = true
+
+# long different strands
+"cd2273a5-c576-46c8-a52b-dee251c3e6e5" = true
+
+# disallow first strand longer
+"919f8ef0-b767-4d1b-8516-6379d07fcb28" = true
+
+# disallow second strand longer
+"8a2d4ed0-ead5-4fdd-924d-27c4cf56e60e" = true
+
+# disallow left empty strand
+"5dce058b-28d4-4ca7-aa64-adfe4e17784c" = true
+
+# disallow right empty strand
+"38826d4b-16fb-4639-ac3e-ba027dec8b5f" = true
diff --git a/exercises/hello-world/.meta/tests.toml b/exercises/hello-world/.meta/tests.toml
new file mode 100644
index 00000000..e64aebeb
--- /dev/null
+++ b/exercises/hello-world/.meta/tests.toml
@@ -0,0 +1,4 @@
+[canonical-tests]
+
+# Say Hi!
+"af9ffe10-dc13-42d8-a742-e7bdafac449d" = true
diff --git a/exercises/high-scores/.meta/tests.toml b/exercises/high-scores/.meta/tests.toml
new file mode 100644
index 00000000..9f98b254
--- /dev/null
+++ b/exercises/high-scores/.meta/tests.toml
@@ -0,0 +1,25 @@
+[canonical-tests]
+
+# List of scores
+"1035eb93-2208-4c22-bab8-fef06769a73c" = true
+
+# Latest score
+"6aa5dbf5-78fa-4375-b22c-ffaa989732d2" = true
+
+# Personal best
+"b661a2e1-aebf-4f50-9139-0fb817dd12c6" = true
+
+# Personal top three from a list of scores
+"3d996a97-c81c-4642-9afc-80b80dc14015" = true
+
+# Personal top highest to lowest
+"1084ecb5-3eb4-46fe-a816-e40331a4e83a" = true
+
+# Personal top when there is a tie
+"e6465b6b-5a11-4936-bfe3-35241c4f4f16" = true
+
+# Personal top when there are less than 3
+"f73b02af-c8fd-41c9-91b9-c86eaa86bce2" = true
+
+# Personal top when there is only one
+"16608eae-f60f-4a88-800e-aabce5df2865" = true
diff --git a/exercises/house/.meta/tests.toml b/exercises/house/.meta/tests.toml
new file mode 100644
index 00000000..067deeb8
--- /dev/null
+++ b/exercises/house/.meta/tests.toml
@@ -0,0 +1,43 @@
+[canonical-tests]
+
+# verse one - the house that jack built
+"28a540ff-f765-4348-9d57-ae33f25f41f2" = true
+
+# verse two - the malt that lay
+"ebc825ac-6e2b-4a5e-9afd-95732191c8da" = true
+
+# verse three - the rat that ate
+"1ed8bb0f-edb8-4bd1-b6d4-b64754fe4a60" = true
+
+# verse four - the cat that killed
+"64b0954e-8b7d-4d14-aad0-d3f6ce297a30" = true
+
+# verse five - the dog that worried
+"1e8d56bc-fe31-424d-9084-61e6111d2c82" = true
+
+# verse six - the cow with the crumpled horn
+"6312dc6f-ab0a-40c9-8a55-8d4e582beac4" = true
+
+# verse seven - the maiden all forlorn
+"68f76d18-6e19-4692-819c-5ff6a7f92feb" = true
+
+# verse eight - the man all tattered and torn
+"73872564-2004-4071-b51d-2e4326096747" = true
+
+# verse nine - the priest all shaven and shorn
+"0d53d743-66cb-4351-a173-82702f3338c9" = true
+
+# verse 10 - the rooster that crowed in the morn
+"452f24dc-8fd7-4a82-be1a-3b4839cfeb41" = true
+
+# verse 11 - the farmer sowing his corn
+"97176f20-2dd3-4646-ac72-cffced91ea26" = true
+
+# verse 12 - the horse and the hound and the horn
+"09824c29-6aad-4dcd-ac98-f61374a6a8b7" = true
+
+# multiple verses
+"d2b980d3-7851-49e1-97ab-1524515ec200" = true
+
+# full rhyme
+"0311d1d0-e085-4f23-8ae7-92406fb3e803" = true
diff --git a/exercises/isbn-verifier/.meta/tests.toml b/exercises/isbn-verifier/.meta/tests.toml
new file mode 100644
index 00000000..39d42f6e
--- /dev/null
+++ b/exercises/isbn-verifier/.meta/tests.toml
@@ -0,0 +1,52 @@
+[canonical-tests]
+
+# valid isbn number
+"0caa3eac-d2e3-4c29-8df8-b188bc8c9292" = true
+
+# invalid isbn check digit
+"19f76b53-7c24-45f8-87b8-4604d0ccd248" = true
+
+# valid isbn number with a check digit of 10
+"4164bfee-fb0a-4a1c-9f70-64c6a1903dcd" = true
+
+# check digit is a character other than X
+"3ed50db1-8982-4423-a993-93174a20825c" = true
+
+# invalid character in isbn
+"c19ba0c4-014f-4dc3-a63f-ff9aefc9b5ec" = true
+
+# X is only valid as a check digit
+"28025280-2c39-4092-9719-f3234b89c627" = true
+
+# valid isbn without separating dashes
+"f6294e61-7e79-46b3-977b-f48789a4945b" = true
+
+# isbn without separating dashes and X as check digit
+"185ab99b-3a1b-45f3-aeec-b80d80b07f0b" = true
+
+# isbn without check digit and dashes
+"7725a837-ec8e-4528-a92a-d981dd8cf3e2" = true
+
+# too long isbn and no dashes
+"47e4dfba-9c20-46ed-9958-4d3190630bdf" = true
+
+# too short isbn
+"737f4e91-cbba-4175-95bf-ae630b41fb60" = true
+
+# isbn without check digit
+"5458a128-a9b6-4ff8-8afb-674e74567cef" = true
+
+# check digit of X should not be used for 0
+"70b6ad83-d0a2-4ca7-a4d5-a9ab731800f7" = true
+
+# empty isbn
+"94610459-55ab-4c35-9b93-ff6ea1a8e562" = true
+
+# input is 9 characters
+"7bff28d4-d770-48cc-80d6-b20b3a0fb46c" = true
+
+# invalid characters are not ignored
+"ed6e8d1b-382c-4081-8326-8b772c581fec" = true
+
+# input is too long but contains a valid isbn
+"fb5e48d8-7c03-4bfb-a088-b101df16fdc3" = true
diff --git a/exercises/isogram/.meta/tests.toml b/exercises/isogram/.meta/tests.toml
new file mode 100644
index 00000000..cfecd1ee
--- /dev/null
+++ b/exercises/isogram/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# empty string
+"a0e97d2d-669e-47c7-8134-518a1e2c4555" = true
+
+# isogram with only lower case characters
+"9a001b50-f194-4143-bc29-2af5ec1ef652" = true
+
+# word with one duplicated character
+"8ddb0ca3-276e-4f8b-89da-d95d5bae78a4" = true
+
+# word with one duplicated character from the end of the alphabet
+"6450b333-cbc2-4b24-a723-0b459b34fe18" = true
+
+# longest reported english isogram
+"a15ff557-dd04-4764-99e7-02cc1a385863" = true
+
+# word with duplicated character in mixed case
+"f1a7f6c7-a42f-4915-91d7-35b2ea11c92e" = true
+
+# word with duplicated character in mixed case, lowercase first
+"14a4f3c1-3b47-4695-b645-53d328298942" = true
+
+# hypothetical isogrammic word with hyphen
+"423b850c-7090-4a8a-b057-97f1cadd7c42" = true
+
+# hypothetical word with duplicated character following hyphen
+"93dbeaa0-3c5a-45c2-8b25-428b8eacd4f2" = true
+
+# isogram with duplicated hyphen
+"36b30e5c-173f-49c6-a515-93a3e825553f" = true
+
+# made-up name that is an isogram
+"cdabafa0-c9f4-4c1f-b142-689c6ee17d93" = true
+
+# duplicated character in the middle
+"5fc61048-d74e-48fd-bc34-abfc21552d4d" = true
+
+# same first and last characters
+"310ac53d-8932-47bc-bbb4-b2b94f25a83e" = true
diff --git a/exercises/kindergarten-garden/.meta/tests.toml b/exercises/kindergarten-garden/.meta/tests.toml
new file mode 100644
index 00000000..ec946c69
--- /dev/null
+++ b/exercises/kindergarten-garden/.meta/tests.toml
@@ -0,0 +1,28 @@
+[canonical-tests]
+
+# garden with single student
+"1fc316ed-17ab-4fba-88ef-3ae78296b692" = true
+
+# different garden with single student
+"acd19dc1-2200-4317-bc2a-08f021276b40" = true
+
+# garden with two students
+"c376fcc8-349c-446c-94b0-903947315757" = true
+
+# second student's garden
+"2d620f45-9617-4924-9d27-751c80d17db9" = true
+
+# third student's garden
+"57712331-4896-4364-89f8-576421d69c44" = true
+
+# first student's garden
+"149b4290-58e1-40f2-8ae4-8b87c46e765b" = true
+
+# second student's garden
+"ba25dbbc-10bd-4a37-b18e-f89ecd098a5e" = true
+
+# second to last student's garden
+"6bb66df7-f433-41ab-aec2-3ead6e99f65b" = true
+
+# last student's garden
+"d7edec11-6488-418a-94e6-ed509e0fa7eb" = true
diff --git a/exercises/knapsack/.meta/tests.toml b/exercises/knapsack/.meta/tests.toml
new file mode 100644
index 00000000..b7dec7da
--- /dev/null
+++ b/exercises/knapsack/.meta/tests.toml
@@ -0,0 +1,22 @@
+[canonical-tests]
+
+# no items
+"a4d7d2f0-ad8a-460c-86f3-88ba709d41a7" = true
+
+# one item, too heavy
+"1d39e98c-6249-4a8b-912f-87cb12e506b0" = true
+
+# five items (cannot be greedy by weight)
+"833ea310-6323-44f2-9d27-a278740ffbd8" = true
+
+# five items (cannot be greedy by value)
+"277cdc52-f835-4c7d-872b-bff17bab2456" = true
+
+# example knapsack
+"81d8e679-442b-4f7a-8a59-7278083916c9" = true
+
+# 8 items
+"f23a2449-d67c-4c26-bf3e-cde020f27ecc" = true
+
+# 15 items
+"7c682ae9-c385-4241-a197-d2fa02c81a11" = true
diff --git a/exercises/largest-series-product/.meta/tests.toml b/exercises/largest-series-product/.meta/tests.toml
new file mode 100644
index 00000000..6bd30c05
--- /dev/null
+++ b/exercises/largest-series-product/.meta/tests.toml
@@ -0,0 +1,46 @@
+[canonical-tests]
+
+# finds the largest product if span equals length
+"7c82f8b7-e347-48ee-8a22-f672323324d4" = true
+
+# can find the largest product of 2 with numbers in order
+"88523f65-21ba-4458-a76a-b4aaf6e4cb5e" = true
+
+# can find the largest product of 2
+"f1376b48-1157-419d-92c2-1d7e36a70b8a" = true
+
+# can find the largest product of 3 with numbers in order
+"46356a67-7e02-489e-8fea-321c2fa7b4a4" = true
+
+# can find the largest product of 3
+"a2dcb54b-2b8f-4993-92dd-5ce56dece64a" = true
+
+# can find the largest product of 5 with numbers in order
+"673210a3-33cd-4708-940b-c482d7a88f9d" = true
+
+# can get the largest product of a big number
+"02acd5a6-3bbf-46df-8282-8b313a80a7c9" = true
+
+# reports zero if the only digits are zero
+"76dcc407-21e9-424c-a98e-609f269622b5" = true
+
+# reports zero if all spans include zero
+"6ef0df9f-52d4-4a5d-b210-f6fae5f20e19" = true
+
+# rejects span longer than string length
+"5d81aaf7-4f67-4125-bf33-11493cc7eab7" = true
+
+# reports 1 for empty string and empty product (0 span)
+"06bc8b90-0c51-4c54-ac22-3ec3893a079e" = true
+
+# reports 1 for nonempty string and empty product (0 span)
+"3ec0d92e-f2e2-4090-a380-70afee02f4c0" = true
+
+# rejects empty string and nonzero span
+"6d96c691-4374-4404-80ee-2ea8f3613dd4" = true
+
+# rejects invalid character in digits
+"7a38f2d6-3c35-45f6-8d6f-12e6e32d4d74" = true
+
+# rejects negative span
+"5fe3c0e5-a945-49f2-b584-f0814b4dd1ef" = true
diff --git a/exercises/leap/.meta/tests.toml b/exercises/leap/.meta/tests.toml
new file mode 100644
index 00000000..8e23f533
--- /dev/null
+++ b/exercises/leap/.meta/tests.toml
@@ -0,0 +1,28 @@
+[canonical-tests]
+
+# year not divisible by 4 in common year
+"6466b30d-519c-438e-935d-388224ab5223" = true
+
+# year divisible by 2, not divisible by 4 in common year
+"ac227e82-ee82-4a09-9eb6-4f84331ffdb0" = true
+
+# year divisible by 4, not divisible by 100 in leap year
+"4fe9b84c-8e65-489e-970b-856d60b8b78e" = true
+
+# year divisible by 4 and 5 is still a leap year
+"7fc6aed7-e63c-48f5-ae05-5fe182f60a5d" = true
+
+# year divisible by 100, not divisible by 400 in common year
+"78a7848f-9667-4192-ae53-87b30c9a02dd" = true
+
+# year divisible by 100 but not by 3 is still not a leap year
+"9d70f938-537c-40a6-ba19-f50739ce8bac" = true
+
+# year divisible by 400 in leap year
+"42ee56ad-d3e6-48f1-8e3f-c84078d916fc" = true
+
+# year divisible by 400 but not by 125 is still a leap year
+"57902c77-6fe9-40de-8302-587b5c27121e" = true
+
+# year divisible by 200, not divisible by 400 in common year
+"c30331f6-f9f6-4881-ad38-8ca8c12520c1" = true
diff --git a/exercises/list-ops/.meta/tests.toml b/exercises/list-ops/.meta/tests.toml
new file mode 100644
index 00000000..7c5aff11
--- /dev/null
+++ b/exercises/list-ops/.meta/tests.toml
@@ -0,0 +1,64 @@
+[canonical-tests]
+
+# empty lists
+"485b9452-bf94-40f7-a3db-c3cf4850066a" = true
+
+# list to empty list
+"2c894696-b609-4569-b149-8672134d340a" = true
+
+# non-empty lists
+"71dcf5eb-73ae-4a0e-b744-a52ee387922f" = true
+
+# empty list
+"28444355-201b-4af2-a2f6-5550227bde21" = true
+
+# list of lists
+"331451c1-9573-42a1-9869-2d06e3b389a9" = true
+
+# list of nested lists
+"d6ecd72c-197f-40c3-89a4-aa1f45827e09" = true
+
+# empty list
+"0524fba8-3e0f-4531-ad2b-f7a43da86a16" = true
+
+# non-empty list
+"88494bd5-f520-4edb-8631-88e415b62d24" = true
+
+# empty list
+"1cf0b92d-8d96-41d5-9c21-7b3c37cb6aad" = true
+
+# non-empty list
+"d7b8d2d9-2d16-44c4-9a19-6e5f237cb71e" = true
+
+# empty list
+"c0bc8962-30e2-4bec-9ae4-668b8ecd75aa" = true
+
+# non-empty list
+"11e71a95-e78b-4909-b8e4-60cdcaec0e91" = true
+
+# empty list
+"613b20b7-1873-4070-a3a6-70ae5f50d7cc" = true
+
+# direction independent function applied to non-empty list
+"e56df3eb-9405-416a-b13a-aabb4c3b5194" = true
+
+# direction dependent function applied to non-empty list
+"d2cf5644-aee1-4dfc-9b88-06896676fe27" = true
+
+# empty list
+"aeb576b9-118e-4a57-a451-db49fac20fdc" = true
+
+# direction independent function applied to non-empty list
+"c4b64e58-313e-4c47-9c68-7764964efb8e" = true
+
+# direction dependent function applied to non-empty list
+"be396a53-c074-4db3-8dd6-f7ed003cce7c" = true
+
+# empty list
+"94231515-050e-4841-943d-d4488ab4ee30" = true
+
+# non-empty list
+"fcc03d1e-42e0-4712-b689-d54ad761f360" = true
+
+# list of lists is not flattened
+"40872990-b5b8-4cb8-9085-d91fc0d05d26" = true
diff --git a/exercises/luhn/.meta/tests.toml b/exercises/luhn/.meta/tests.toml
new file mode 100644
index 00000000..2b441c6f
--- /dev/null
+++ b/exercises/luhn/.meta/tests.toml
@@ -0,0 +1,55 @@
+[canonical-tests]
+
+# single digit strings can not be valid
+"792a7082-feb7-48c7-b88b-bbfec160865e" = true
+
+# a single zero is invalid
+"698a7924-64d4-4d89-8daa-32e1aadc271e" = true
+
+# a simple valid SIN that remains valid if reversed
+"73c2f62b-9b10-4c9f-9a04-83cee7367965" = true
+
+# a simple valid SIN that becomes invalid if reversed
+"9369092e-b095-439f-948d-498bd076be11" = true
+
+# a valid Canadian SIN
+"8f9f2350-1faf-4008-ba84-85cbb93ffeca" = true
+
+# invalid Canadian SIN
+"1cdcf269-6560-44fc-91f6-5819a7548737" = true
+
+# invalid credit card
+"656c48c1-34e8-4e60-9a5a-aad8a367810a" = true
+
+# invalid long number with an even remainder
+"20e67fad-2121-43ed-99a8-14b5b856adb9" = true
+
+# valid number with an even number of digits
+"ad2a0c5f-84ed-4e5b-95da-6011d6f4f0aa" = true
+
+# valid number with an odd number of spaces
+"ef081c06-a41f-4761-8492-385e13c8202d" = true
+
+# valid strings with a non-digit added at the end become invalid
+"bef66f64-6100-4cbb-8f94-4c9713c5e5b2" = true
+
+# valid strings with punctuation included become invalid
+"2177e225-9ce7-40f6-b55d-fa420e62938e" = true
+
+# valid strings with symbols included become invalid
+"ebf04f27-9698-45e1-9afe-7e0851d0fe8d" = true
+
+# single zero with space is invalid
+"08195c5e-ce7f-422c-a5eb-3e45fece68ba" = true
+
+# more than a single zero is valid
+"12e63a3c-f866-4a79-8c14-b359fc386091" = true
+
+# input digit 9 is correctly converted to output digit 9
+"ab56fa80-5de8-4735-8a4a-14dae588663e" = true
+
+# using ascii value for non-doubled non-digit isn't allowed
+"39a06a5a-5bad-4e0f-b215-b042d46209b1" = true
+
+# using ascii value for doubled non-digit isn't allowed
+"f94cf191-a62f-4868-bc72-7253114aa157" = true
diff --git a/exercises/markdown/.meta/tests.toml b/exercises/markdown/.meta/tests.toml
new file mode 100644
index 00000000..918edb01
--- /dev/null
+++ b/exercises/markdown/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# parses normal text as a paragraph
+"e75c8103-a6b8-45d9-84ad-e68520545f6e" = true
+
+# parsing italics
+"69a4165d-9bf8-4dd7-bfdc-536eaca80a6a" = true
+
+# parsing bold text
+"ec345a1d-db20-4569-a81a-172fe0cad8a1" = true
+
+# mixed normal, italics and bold text
+"51164ed4-5641-4909-8fab-fbaa9d37d5a8" = true
+
+# with h1 header level
+"ad85f60d-0edd-4c6a-a9b1-73e1c4790d15" = true
+
+# with h2 header level
+"d0f7a31f-6935-44ac-8a9a-1e8ab16af77f" = true
+
+# with h6 header level
+"13b5f410-33f5-44f0-a6a7-cfd4ab74b5d5" = true
+
+# unordered lists
+"25288a2b-8edc-45db-84cf-0b6c6ee034d6" = true
+
+# With a little bit of everything
+"7bf92413-df8f-4de8-9184-b724f363c3da" = true
+
+# with markdown symbols in the header text that should not be interpreted
+"0b3ed1ec-3991-4b8b-8518-5cb73d4a64fe" = true
+
+# with markdown symbols in the list item text that should not be interpreted
+"113a2e58-78de-4efa-90e9-20972224d759" = true
+
+# with markdown symbols in the paragraph text that should not be interpreted
+"e65e46e2-17b7-4216-b3ac-f44a1b9bcdb4" = true
+
+# unordered lists close properly with preceding and following lines
+"f0bbbbde-0f52-4c0c-99ec-be4c60126dd4" = true
diff --git a/exercises/matching-brackets/.meta/tests.toml b/exercises/matching-brackets/.meta/tests.toml
new file mode 100644
index 00000000..98309c73
--- /dev/null
+++ b/exercises/matching-brackets/.meta/tests.toml
@@ -0,0 +1,52 @@
+[canonical-tests]
+
+# paired square brackets
+"81ec11da-38dd-442a-bcf9-3de7754609a5" = true
+
+# empty string
+"287f0167-ac60-4b64-8452-a0aa8f4e5238" = true
+
+# unpaired brackets
+"6c3615a3-df01-4130-a731-8ef5f5d78dac" = true
+
+# wrong ordered brackets
+"9d414171-9b98-4cac-a4e5-941039a97a77" = true
+
+# wrong closing bracket
+"f0f97c94-a149-4736-bc61-f2c5148ffb85" = true
+
+# paired with whitespace
+"754468e0-4696-4582-a30e-534d47d69756" = true
+
+# partially paired brackets
+"ba84f6ee-8164-434a-9c3e-b02c7f8e8545" = true
+
+# simple nested brackets
+"3c86c897-5ff3-4a2b-ad9b-47ac3a30651d" = true
+
+# several paired brackets
+"2d137f2c-a19e-4993-9830-83967a2d4726" = true
+
+# paired and nested brackets
+"2e1f7b56-c137-4c92-9781-958638885a44" = true
+
+# unopened closing brackets
+"84f6233b-e0f7-4077-8966-8085d295c19b" = true
+
+# unpaired and nested brackets
+"9b18c67d-7595-4982-b2c5-4cb949745d49" = true
+
+# paired and wrong nested brackets
+"a0205e34-c2ac-49e6-a88a-899508d7d68e" = true
+
+# paired and incomplete brackets
+"ef47c21b-bcfd-4998-844c-7ad5daad90a8" = true
+
+# too many closing brackets
+"a4675a40-a8be-4fc2-bc47-2a282ce6edbe" = true
+
+# math expression
+"99255f93-261b-4435-a352-02bdecc9bdf2" = true
+
+# complex latex expression
+"8e357d79-f302-469a-8515-2561877256a1" = true
diff --git a/exercises/matrix/.meta/tests.toml b/exercises/matrix/.meta/tests.toml
new file mode 100644
index 00000000..986d6dd1
--- /dev/null
+++ b/exercises/matrix/.meta/tests.toml
@@ -0,0 +1,25 @@
+[canonical-tests]
+
+# extract row from one number matrix
+"ca733dab-9d85-4065-9ef6-a880a951dafd" = true
+
+# can extract row
+"5c93ec93-80e1-4268-9fc2-63bc7d23385c" = true
+
+# extract row where numbers have different widths
+"2f1aad89-ad0f-4bd2-9919-99a8bff0305a" = true
+
+# can extract row from non-square matrix with no corresponding column
+"68f7f6ba-57e2-4e87-82d0-ad09889b5204" = true
+
+# extract column from one number matrix
+"e8c74391-c93b-4aed-8bfe-f3c9beb89ebb" = true
+
+# can extract column
+"7136bdbd-b3dc-48c4-a10c-8230976d3727" = true
+
+# can extract column from non-square matrix with no corresponding row
+"ad64f8d7-bba6-4182-8adf-0c14de3d0eca" = true
+
+# extract column where numbers have different widths
+"9eddfa5c-8474-440e-ae0a-f018c2a0dd89" = true
diff --git a/exercises/meetup/.meta/tests.toml b/exercises/meetup/.meta/tests.toml
new file mode 100644
index 00000000..45366ba1
--- /dev/null
+++ b/exercises/meetup/.meta/tests.toml
@@ -0,0 +1,286 @@
+[canonical-tests]
+
+# monteenth of May 2013
+"d7f8eadd-d4fc-46ee-8a20-e97bd3fd01c8" = true
+
+# monteenth of August 2013
+"f78373d1-cd53-4a7f-9d37-e15bf8a456b4" = true
+
+# monteenth of September 2013
+"8c78bea7-a116-425b-9c6b-c9898266d92a" = true
+
+# tuesteenth of March 2013
+"cfef881b-9dc9-4d0b-8de4-82d0f39fc271" = true
+
+# tuesteenth of April 2013
+"69048961-3b00-41f9-97ee-eb6d83a8e92b" = true
+
+# tuesteenth of August 2013
+"d30bade8-3622-466a-b7be-587414e0caa6" = true
+
+# wednesteenth of January 2013
+"8db4b58b-92f3-4687-867b-82ee1a04f851" = true
+
+# wednesteenth of February 2013
+"6c27a2a2-28f8-487f-ae81-35d08c4664f7" = true
+
+# wednesteenth of June 2013
+"008a8674-1958-45b5-b8e6-c2c9960d973a" = true
+
+# thursteenth of May 2013
+"e4abd5e3-57cb-4091-8420-d97e955c0dbd" = true
+
+# thursteenth of June 2013
+"85da0b0f-eace-4297-a6dd-63588d5055b4" = true
+
+# thursteenth of September 2013
+"ecf64f9b-8413-489b-bf6e-128045f70bcc" = true
+
+# friteenth of April 2013
+"ac4e180c-7d0a-4d3d-b05f-f564ebb584ca" = true
+
+# friteenth of August 2013
+"b79101c7-83ad-4f8f-8ec8-591683296315" = true
+
+# friteenth of September 2013
+"6ed38b9f-0072-4901-bd97-7c8b8b0ef1b8" = true
+
+# saturteenth of February 2013
+"dfae03ed-9610-47de-a632-655ab01e1e7c" = true
+
+# saturteenth of April 2013
+"ec02e3e1-fc72-4a3c-872f-a53fa8ab358e" = true
+
+# saturteenth of October 2013
+"d983094b-7259-4195-b84e-5d09578c89d9" = true
+
+# sunteenth of May 2013
+"d84a2a2e-f745-443a-9368-30051be60c2e" = true
+
+# sunteenth of June 2013
+"0e64bc53-92a3-4f61-85b2-0b7168c7ce5a" = true
+
+# sunteenth of October 2013
+"de87652c-185e-4854-b3ae-04cf6150eead" = true
+
+# first Monday of March 2013
+"2cbfd0f5-ba3a-46da-a8cc-0fe4966d3411" = true
+
+# first Monday of April 2013
+"a6168c7c-ed95-4bb3-8f92-c72575fc64b0" = true
+
+# first Tuesday of May 2013
+"1bfc620f-1c54-4bbd-931f-4a1cd1036c20" = true
+
+# first Tuesday of June 2013
+"12959c10-7362-4ca0-a048-50cf1c06e3e2" = true
+
+# first Wednesday of July 2013
+"1033dc66-8d0b-48a1-90cb-270703d59d1d" = true
+
+# first Wednesday of August 2013
+"b89185b9-2f32-46f4-a602-de20b09058f6" = true
+
+# first Thursday of September 2013
+"53aedc4d-b2c8-4dfb-abf7-a8dc9cdceed5" = true
+
+# first Thursday of October 2013
+"b420a7e3-a94c-4226-870a-9eb3a92647f0" = true
+
+# first Friday of November 2013
+"61df3270-28b4-4713-bee2-566fa27302ca" = true
+
+# first Friday of December 2013
+"cad33d4d-595c-412f-85cf-3874c6e07abf" = true
+
+# first Saturday of January 2013
+"a2869b52-5bba-44f0-a863-07bd1f67eadb" = true
+
+# first Saturday of February 2013
+"3585315a-d0db-4ea1-822e-0f22e2a645f5" = true
+
+# first Sunday of March 2013
+"c49e9bd9-8ccf-4cf2-947a-0ccd4e4f10b1" = true
+
+# first Sunday of April 2013
+"1513328b-df53-4714-8677-df68c4f9366c" = true
+
+# second Monday of March 2013
+"49e083af-47ec-4018-b807-62ef411efed7" = true
+
+# second Monday of April 2013
+"6cb79a73-38fe-4475-9101-9eec36cf79e5" = true
+
+# second Tuesday of May 2013
+"4c39b594-af7e-4445-aa03-bf4f8effd9a1" = true
+
+# second Tuesday of June 2013
+"41b32c34-2e39-40e3-b790-93539aaeb6dd" = true
+
+# second Wednesday of July 2013
+"90a160c5-b5d9-4831-927f-63a78b17843d" = true
+
+# second Wednesday of August 2013
+"23b98ce7-8dd5-41a1-9310-ef27209741cb" = true
+
+# second Thursday of September 2013
+"447f1960-27ca-4729-bc3f-f36043f43ed0" = true
+
+# second Thursday of October 2013
+"c9aa2687-300c-4e79-86ca-077849a81bde" = true
+
+# second Friday of November 2013
+"a7e11ef3-6625-4134-acda-3e7195421c09" = true
+
+# second Friday of December 2013
+"8b420e5f-9290-4106-b5ae-022f3e2a3e41" = true
+
+# second Saturday of January 2013
+"80631afc-fc11-4546-8b5f-c12aaeb72b4f" = true
+
+# second Saturday of February 2013
+"e34d43ac-f470-44c2-aa5f-e97b78ecaf83" = true
+
+# second Sunday of March 2013
+"a57d59fd-1023-47ad-b0df-a6feb21b44fc" = true
+
+# second Sunday of April 2013
+"a829a8b0-abdd-4ad1-b66c-5560d843c91a" = true
+
+# third Monday of March 2013
+"501a8a77-6038-4fc0-b74c-33634906c29d" = true
+
+# third Monday of April 2013
+"49e4516e-cf32-4a58-8bbc-494b7e851c92" = true
+
+# third Tuesday of May 2013
+"4db61095-f7c7-493c-85f1-9996ad3012c7" = true
+
+# third Tuesday of June 2013
+"714fc2e3-58d0-4b91-90fd-61eefd2892c0" = true
+
+# third Wednesday of July 2013
+"b08a051a-2c80-445b-9b0e-524171a166d1" = true
+
+# third Wednesday of August 2013
+"80bb9eff-3905-4c61-8dc9-bb03016d8ff8" = true
+
+# third Thursday of September 2013
+"fa52a299-f77f-4784-b290-ba9189fbd9c9" = true
+
+# third Thursday of October 2013
+"f74b1bc6-cc5c-4bf1-ba69-c554a969eb38" = true
+
+# third Friday of November 2013
+"8900f3b0-801a-466b-a866-f42d64667abd" = true
+
+# third Friday of December 2013
+"538ac405-a091-4314-9ccd-920c4e38e85e" = true
+
+# third Saturday of January 2013
+"244db35c-2716-4fa0-88ce-afd58e5cf910" = true
+
+# third Saturday of February 2013
+"dd28544f-f8fa-4f06-9bcd-0ad46ce68e9e" = true
+
+# third Sunday of March 2013
+"be71dcc6-00d2-4b53-a369-cbfae55b312f" = true
+
+# third Sunday of April 2013
+"b7d2da84-4290-4ee6-a618-ee124ae78be7" = true
+
+# fourth Monday of March 2013
+"4276dc06-a1bd-4fc2-b6c2-625fee90bc88" = true
+
+# fourth Monday of April 2013
+"ddbd7976-2deb-4250-8a38-925ac1a8e9a2" = true
+
+# fourth Tuesday of May 2013
+"eb714ef4-1656-47cc-913c-844dba4ebddd" = true
+
+# fourth Tuesday of June 2013
+"16648435-7937-4d2d-b118-c3e38fd084bd" = true
+
+# fourth Wednesday of July 2013
+"de062bdc-9484-437a-a8c5-5253c6f6785a" = true
+
+# fourth Wednesday of August 2013
+"c2ce6821-169c-4832-8d37-690ef5d9514a" = true
+
+# fourth Thursday of September 2013
+"d462c631-2894-4391-a8e3-dbb98b7a7303" = true
+
+# fourth Thursday of October 2013
+"9ff1f7b6-1b72-427d-9ee9-82b5bb08b835" = true
+
+# fourth Friday of November 2013
+"83bae8ba-1c49-49bc-b632-b7c7e1d7e35f" = true
+
+# fourth Friday of December 2013
+"de752d2a-a95e-48d2-835b-93363dac3710" = true
+
+# fourth Saturday of January 2013
+"eedd90ad-d581-45db-8312-4c6dcf9cf560" = true
+
+# fourth Saturday of February 2013
+"669fedcd-912e-48c7-a0a1-228b34af91d0" = true
+
+# fourth Sunday of March 2013
+"648e3849-ea49-44a5-a8a3-9f2a43b3bf1b" = true
+
+# fourth Sunday of April 2013
+"f81321b3-99ab-4db6-9267-69c5da5a7823" = true
+
+# last Monday of March 2013
+"1af5e51f-5488-4548-aee8-11d7d4a730dc" = true
+
+# last Monday of April 2013
+"f29999f2-235e-4ec7-9dab-26f137146526" = true
+
+# last Tuesday of May 2013
+"31b097a0-508e-48ac-bf8a-f63cdcf6dc41" = true
+
+# last Tuesday of June 2013
+"8c022150-0bb5-4a1f-80f9-88b2e2abcba4" = true
+
+# last Wednesday of July 2013
+"0e762194-672a-4bdf-8a37-1e59fdacef12" = true
+
+# last Wednesday of August 2013
+"5016386a-f24e-4bd7-b439-95358f491b66" = true
+
+# last Thursday of September 2013
+"12ead1a5-cdf9-4192-9a56-2229e93dd149" = true
+
+# last Thursday of October 2013
+"7db89e11-7fbe-4e57-ae3c-0f327fbd7cc7" = true
+
+# last Friday of November 2013
+"e47a739e-b979-460d-9c8a-75c35ca2290b" = true
+
+# last Friday of December 2013
+"5bed5aa9-a57a-4e5d-8997-2cc796a5b0ec" = true
+
+# last Saturday of January 2013
+"61e54cba-76f3-4772-a2b1-bf443fda2137" = true
+
+# last Saturday of February 2013
+"8b6a737b-2fa9-444c-b1a2-80ce7a2ec72f" = true
+
+# last Sunday of March 2013
+"0b63e682-f429-4d19-9809-4a45bd0242dc" = true
+
+# last Sunday of April 2013
+"5232307e-d3e3-4afc-8ba6-4084ad987c00" = true
+
+# last Wednesday of February 2012
+"0bbd48e8-9773-4e81-8e71-b9a51711e3c5" = true
+
+# last Wednesday of December 2014
+"fe0936de-7eee-4a48-88dd-66c07ab1fefc" = true
+
+# last Sunday of February 2015
+"2ccf2488-aafc-4671-a24e-2b6effe1b0e2" = true
+
+# first Friday of December 2012
+"00c3ce9f-cf36-4b70-90d8-92b32be6830e" = true
diff --git a/exercises/minesweeper/.meta/tests.toml b/exercises/minesweeper/.meta/tests.toml
new file mode 100644
index 00000000..e91b16d0
--- /dev/null
+++ b/exercises/minesweeper/.meta/tests.toml
@@ -0,0 +1,37 @@
+[canonical-tests]
+
+# no rows
+"0c5ec4bd-dea7-4138-8651-1203e1cb9f44" = true
+
+# no columns
+"650ac4c0-ad6b-4b41-acde-e4ea5852c3b8" = true
+
+# no mines
+"6fbf8f6d-a03b-42c9-9a58-b489e9235478" = true
+
+# minefield with only mines
+"61aff1c4-fb31-4078-acad-cd5f1e635655" = true
+
+# mine surrounded by spaces
+"84167147-c504-4896-85d7-246b01dea7c5" = true
+
+# space surrounded by mines
+"cb878f35-43e3-4c9d-93d9-139012cccc4a" = true
+
+# horizontal line
+"7037f483-ddb4-4b35-b005-0d0f4ef4606f" = true
+
+# horizontal line, mines at edges
+"e359820f-bb8b-4eda-8762-47b64dba30a6" = true
+
+# vertical line
+"c5198b50-804f-47e9-ae02-c3b42f7ce3ab" = true
+
+# vertical line, mines at edges
+"0c79a64d-703d-4660-9e90-5adfa5408939" = true
+
+# cross
+"4b098563-b7f3-401c-97c6-79dd1b708f34" = true
+
+# large minefield
+"04a260f1-b40a-4e89-839e-8dd8525abe0e" = true
diff --git a/exercises/nth-prime/.meta/tests.toml b/exercises/nth-prime/.meta/tests.toml
new file mode 100644
index 00000000..62e0316a
--- /dev/null
+++ b/exercises/nth-prime/.meta/tests.toml
@@ -0,0 +1,16 @@
+[canonical-tests]
+
+# first prime
+"75c65189-8aef-471a-81de-0a90c728160c" = true
+
+# second prime
+"2c38804c-295f-4701-b728-56dea34fd1a0" = true
+
+# sixth prime
+"56692534-781e-4e8c-b1f9-3e82c1640259" = true
+
+# big prime
+"fce1e979-0edb-412d-93aa-2c744e8f50ff" = true
+
+# there is no zeroth prime
+"bd0a9eae-6df7-485b-a144-80e13c7d55b2" = true
diff --git a/exercises/nucleotide-count/.meta/tests.toml b/exercises/nucleotide-count/.meta/tests.toml
new file mode 100644
index 00000000..f0a57da7
--- /dev/null
+++ b/exercises/nucleotide-count/.meta/tests.toml
@@ -0,0 +1,16 @@
+[canonical-tests]
+
+# empty strand
+"3e5c30a8-87e2-4845-a815-a49671ade970" = true
+
+# can count one nucleotide in single-character input
+"a0ea42a6-06d9-4ac6-828c-7ccaccf98fec" = true
+
+# strand with repeated nucleotide
+"eca0d565-ed8c-43e7-9033-6cefbf5115b5" = true
+
+# strand with multiple nucleotides
+"40a45eac-c83f-4740-901a-20b22d15a39f" = true
+
+# strand with invalid nucleotides
+"b4c47851-ee9e-4b0a-be70-a86e343bd851" = true
diff --git a/exercises/ocr-numbers/.meta/tests.toml b/exercises/ocr-numbers/.meta/tests.toml
new file mode 100644
index 00000000..ac760140
--- /dev/null
+++ b/exercises/ocr-numbers/.meta/tests.toml
@@ -0,0 +1,52 @@
+[canonical-tests]
+
+# Recognizes 0
+"5ee54e1a-b554-4bf3-a056-9a7976c3f7e8" = true
+
+# Recognizes 1
+"027ada25-17fd-4d78-aee6-35a19623639d" = true
+
+# Unreadable but correctly sized inputs return ?
+"3cce2dbd-01d9-4f94-8fae-419a822e89bb" = true
+
+# Input with a number of lines that is not a multiple of four raises an error
+"cb19b733-4e36-4cf9-a4a1-6e6aac808b9a" = true
+
+# Input with a number of columns that is not a multiple of three raises an error
+"235f7bd1-991b-4587-98d4-84206eec4cc6" = true
+
+# Recognizes 110101100
+"4a841794-73c9-4da9-a779-1f9837faff66" = true
+
+# Garbled numbers in a string are replaced with ?
+"70c338f9-85b1-4296-a3a8-122901cdfde8" = true
+
+# Recognizes 2
+"ea494ff4-3610-44d7-ab7e-72fdef0e0802" = true
+
+# Recognizes 3
+"1acd2c00-412b-4268-93c2-bd7ff8e05a2c" = true
+
+# Recognizes 4
+"eaec6a15-be17-4b6d-b895-596fae5d1329" = true
+
+# Recognizes 5
+"440f397a-f046-4243-a6ca-81ab5406c56e" = true
+
+# Recognizes 6
+"f4c9cf6a-f1e2-4878-bfc3-9b85b657caa0" = true
+
+# Recognizes 7
+"e24ebf80-c611-41bb-a25a-ac2c0f232df5" = true
+
+# Recognizes 8
+"b79cad4f-e264-4818-9d9e-77766792e233" = true
+
+# Recognizes 9
+"5efc9cfc-9227-4688-b77d-845049299e66" = true
+
+# Recognizes string of decimal numbers
+"f60cb04a-42be-494e-a535-3451c8e097a4" = true
+
+# Numbers separated by empty lines are recognized. Lines are joined by commas.
+"b73ecf8b-4423-4b36-860d-3710bdb8a491" = true
diff --git a/exercises/paasio/paasio_test.py b/exercises/paasio/paasio_test.py
new file mode 100644
index 00000000..9e3612b5
--- /dev/null
+++ b/exercises/paasio/paasio_test.py
@@ -0,0 +1,884 @@
+import errno
+import inspect
+import io
+import logging
+import os
+import unittest
+from unittest.mock import ANY, call, NonCallableMagicMock, patch
+
+from paasio import MeteredFile, MeteredSocket
+
+
+LOGGER = logging.getLogger(__name__)
+
+ZEN = b"""Beautiful is better than ugly.
+Explicit is better than implicit.
+Simple is better than complex.
+Complex is better than complicated.
+Flat is better than nested.
+Sparse is better than dense.
+Readability counts.
+Special cases aren't special enough to break the rules.
+Although practicality beats purity.
+Errors should never pass silently.
+Unless explicitly silenced.
+In the face of ambiguity, refuse the temptation to guess.
+There should be one-- and preferably only one --obvious way to do it.
+Although that way may not be obvious at first unless you're Dutch.
+Now is better than never.
+Although never is often better than *right* now.
+If the implementation is hard to explain, it's a bad idea.
+If the implementation is easy to explain, it may be a good idea.
+Namespaces are one honking great idea -- let's do more of those!
+"""
+
+
+# Implementation tests begin on line 456
+# Code below this line consists of mocks and tests for mocks compliance
+
+
+class MockException(Exception):
+    pass
+
+
+class MockFile(io.BytesIO):
+    def __init__(self, *args, chunk=None, exception=None, **kwargs):
+        super(MockFile, self).__init__(*args, **kwargs)
+        self.__chunk = chunk
+        self.__exception = exception
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        ret = super(MockFile, self).__exit__(exc_type, exc_val, exc_tb)
+        if exc_type is not None and "suppress" in exc_val.args[0]:
+            return True
+        return ret
+
+    def read(self, size=-1):
+        if self.__exception is not None:
+            raise self.__exception
+        if self.__chunk is None:
+            return super(MockFile, self).read(size)
+        if size is None:
+            return super(MockFile, self).read(self.__chunk)
+        if size < 0:
+            return super(MockFile, self).read(self.__chunk)
+        return super(MockFile, self).read(min(self.__chunk, size))
+
+    def write(self, data):
+        if self.__chunk is None:
+            return super(MockFile, self).write(data)
+        return super(MockFile, self).write(data[: self.__chunk])
+
+
+class MockSock:
+    def __init__(self, *, chunk=None, exception=None):
+        self._recver = io.BytesIO(ZEN)
+        self._sender = io.BytesIO()
+        self.__closed = False
+        self.__chunk = chunk
+        self.__exception = exception
+        self.flags = None
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self._recver.close()
+        self._sender.close()
+        self.__closed = True
+        if exc_type is not None and "suppress" in exc_val.args[0]:
+            return True
+        return False
+
+    def recv(self, bufsize, flags=0):
+        if self.__closed:
+            raise OSError(errno.EBADF, os.strerror(errno.EBADF))
+        if bufsize is None:
+            raise TypeError(
+                "'NoneType' object cannot be interpreted as an integer"
+            )
+        if not isinstance(flags, int):
+            raise TypeError(
+                "an integer is required (got type {})".format(
+                    type(flags).__name__
+                )
+            )
+        self.flags = flags
+        if self.__exception is not None:
+            raise self.__exception
+        if self.__chunk is None:
+            return self._recver.read(bufsize)
+        else:
+            return self._recver.read(min(self.__chunk, bufsize))
+
+    def send(self, data, flags=0):
+        if self.__closed:
+            raise OSError(errno.EBADF, os.strerror(errno.EBADF))
+        if not isinstance(flags, int):
+            raise TypeError(
+                "an integer is required (got type {})".format(
+                    type(flags).__name__
+                )
+            )
+        self.flags = flags
+        if self.__chunk is None:
+            return self._sender.write(data)
+        return self._sender.write(data[: self.__chunk])
+
+
+class SuperMock:
+    """Mock for super().__init__ calls only, as mock.MagicMock cannot."""
+
+    def __init__(self, *args, **kwargs):
+        if self.initialized:
+            self.init_called += 1
+        else:
+            self.initialized = True
+
+    def __call__(self, *args, **kwargs):
+        frame = inspect.currentframe()
+        if frame is None:
+            raise RuntimeError("Could not get current frame object")
+        stack = inspect.getouterframes(frame)
+        if any(
+            frame[3] == "__init__" and "paasio" in frame[1] for frame in stack
+        ):
+            return self
+        else:
+            return self.mock_object
+
+    def __repr__(self):
+        return "<SuperMock at {} with mock object: {!r}>".format(
+            hex(id(self)), self.mock_object
+        )
+
+    mock_object = None
+    init_called = 0
+    initialized = False
+
+
+class MockTestCase(unittest.TestCase):
+    def setUp(self):
+        self.test_passed = False
+
+    def tearDown(self):
+        if not self.test_passed:
+            LOGGER.critical(
+                "\nError in mocks, please report to Exercism: %s", self.id()
+            )
+
+    def test_fixture_exception(self):
+        self.assertEqual(False, self.test_passed)
+        with self.assertLogs(__name__, level="CRITICAL") as logs:
+            self.tearDown()
+        self.assertEqual(1, len(logs.output))
+        self.assertRegex(logs.output[0], ":\nError in mocks")
+        self.assertRegex(logs.output[0], "{}$".format(self.id()))
+        self.test_passed = True
+
+
+class SuperMockTest(MockTestCase):
+    def test_super_mock_repr(self):
+        mock = SuperMock()
+        expected = "<SuperMock at {} with mock object: {}>".format(
+            hex(id(mock)), repr(None)
+        )
+        self.assertEqual(expected, repr(mock))
+        file = MockFile()
+        mock.mock_object = file
+        expected = "<SuperMock at {} with mock object: {}>".format(
+            hex(id(mock)), repr(file)
+        )
+        self.assertEqual(expected, repr(mock))
+        self.test_passed = True
+
+    def test_super_mock_init(self):
+        self.assertIs(False, SuperMock.initialized)
+        mock = SuperMock()
+        self.assertIs(True, mock.initialized)
+        for calls in range(5):
+            self.assertEqual(calls, mock.init_called)
+            mock.__init__()
+        self.test_passed = True
+
+    def test_super_mock_call(self):
+        def call2(obj):
+            return obj()
+
+        def __init__(obj):
+            ret = call2(obj)
+            self.assertEqual(obj(), ret)
+            return ret
+
+        mock = SuperMock()
+        self.assertIs(None, mock.mock_object)
+        wrapped = object()
+        mock.mock_object = wrapped
+        self.assertIs(wrapped, mock())
+        self.assertIs(mock, __init__(mock))
+        self.test_passed = True
+
+    @patch("inspect.currentframe", return_value=None)
+    def test_super_mock_frame_error(self, inspect_mock):
+        with self.assertRaisesRegex(
+            RuntimeError, "^Could not get current frame object$"
+        ):
+            SuperMock()()
+        self.test_passed = True
+
+
+class MockSockTest(MockTestCase):
+    def test_context_manager(self):
+        with MockSock() as socket:
+            socket.recv(30)
+            socket.send(b"data")
+        with self.assertRaisesRegex(OSError, os.strerror(errno.EBADF)):
+            socket.recv(30)
+        with self.assertRaisesRegex(OSError, os.strerror(errno.EBADF)):
+            socket.send(b"")
+        self.test_passed = True
+
+    def test_context_manager_exception_raise(self):
+        exception = MockException("Should raise")
+        with self.assertRaisesRegex(MockException, "Should raise") as err:
+            with MockSock(exception=exception) as socket:
+                socket.recv(4096)
+        self.assertEqual(exception, err.exception)
+        self.test_passed = True
+
+    def test_context_manager_exception_suppress(self):
+        exception = MockException("Should suppress")
+        with MockSock(exception=exception) as socket:
+            socket.recv(4096)
+        self.test_passed = True
+
+    def test_recv_once(self):
+        with MockSock() as socket:
+            actual_recv = socket.recv(4096)
+        self.assertEqual(ZEN, actual_recv)
+        self.test_passed = True
+
+    def test_recv_multiple(self):
+        actual_recv = b""
+        with MockSock() as socket:
+            for _ in range(5):
+                actual_recv += socket.recv(30)
+        self.assertEqual(ZEN[:150], actual_recv)
+        self.test_passed = True
+
+    def test_recv_multiple_chunk(self):
+        actual_recv = b""
+        with MockSock(chunk=20) as socket:
+            for _ in range(5):
+                actual_recv += socket.recv(4096)
+            actual_recv += socket.recv(10)
+        self.assertEqual(ZEN[:110], actual_recv)
+        self.test_passed = True
+
+    def test_recv_under_size(self):
+        with MockSock(chunk=257) as socket:
+            actual_recv = socket.recv(4096)
+        self.assertEqual(ZEN[:257], actual_recv)
+        self.test_passed = True
+
+    def test_send_once(self):
+        with MockSock(chunk=257) as socket:
+            send_len = socket.send(ZEN)
+            self.assertEqual(ZEN[:257], socket._sender.getbuffer())
+        self.assertEqual(257, send_len)
+        self.test_passed = True
+
+    def test_send_multiple(self):
+        send_len = 0
+        expected = b"Tomorrow's victory is today's practice."
+        with MockSock() as socket:
+            send_len += socket.send(b"Tomorro")
+            send_len += socket.send(b"w's victo")
+            send_len += socket.send(b"ry is today")
+            send_len += socket.send(b"'s practice.")
+            self.assertEqual(expected, socket._sender.getbuffer())
+        self.assertEqual(39, send_len)
+        self.test_passed = True
+
+    def test_send_under_size(self):
+        with MockSock(chunk=257) as socket:
+            send_len = socket.send(ZEN[:123])
+            self.assertEqual(ZEN[:123], socket._sender.getbuffer())
+        self.assertEqual(123, send_len)
+        self.test_passed = True
+
+    def test_bufsize_required(self):
+        with self.assertRaisesRegex(TypeError, "argument"):
+            with MockSock() as socket:
+                socket.recv()
+
+        with self.assertRaisesRegex(TypeError, "'NoneType'.+integer"):
+            with MockSock() as socket:
+                socket.recv(None)
+        self.test_passed = True
+
+    def test_flags_support(self):
+        with MockSock() as socket:
+            self.assertEqual(len(ZEN), socket.send(ZEN, 42))
+            self.assertEqual(ZEN, socket.recv(4096, 24))
+        with MockSock() as mock_sock:
+            self.assertIs(None, mock_sock.flags)
+            mock_sock.recv(50)
+            self.assertEqual(0, mock_sock.flags)
+            mock_sock.send(b"no flags")
+            self.assertEqual(0, mock_sock.flags)
+            mock_sock.recv(30, 30)
+            self.assertEqual(30, mock_sock.flags)
+            mock_sock.send(b"flags", 1024)
+            self.assertEqual(
+                1024, mock_sock.flags,
+            )
+            with self.assertRaisesRegex(
+                TypeError, r"^an integer is.+NoneType\)$"
+            ):
+                mock_sock.send(b"data", None)
+            with self.assertRaisesRegex(
+                TypeError, r"^an integer is.+bytes\)$"
+            ):
+                mock_sock.send(b"data", b"flags")
+            with self.assertRaisesRegex(
+                TypeError, r"^an integer is.+NoneType\)$"
+            ):
+                mock_sock.recv(b"data", None)
+            with self.assertRaisesRegex(
+                TypeError, r"^an integer is.+bytes\)$"
+            ):
+                mock_sock.recv(b"data", b"flags")
+        self.test_passed = True
+
+
+class MockFileTest(MockTestCase):
+    def test_context_manager(self):
+        with MockFile(ZEN) as file:
+            file.read()
+        with self.assertRaisesRegex(
+            ValueError, "I/O operation on closed file."
+        ):
+            file.read()
+        with self.assertRaisesRegex(
+            ValueError, "I/O operation on closed file."
+        ):
+            file.write(b"data")
+        self.test_passed = True
+
+    def test_context_manager_exception_raise(self):
+        exception = MockException("Should raise")
+        mock = MockFile(ZEN, exception=exception)
+        with self.assertRaisesRegex(MockException, "Should raise") as err:
+            with mock as file:
+                file.read()
+        self.assertEqual(exception, err.exception)
+        self.test_passed = True
+
+    def test_context_manager_exception_suppress(self):
+        exception = MockException("Should suppress")
+        mock = MockFile(ZEN, exception=exception)
+        with mock as file:
+            file.read()
+        self.test_passed = True
+
+    def test_read_once(self):
+        with MockFile(ZEN) as file:
+            actual_read = file.read()
+        self.assertEqual(ZEN, actual_read)
+
+        with MockFile(ZEN) as file:
+            actual_read = file.read(None)
+        self.assertEqual(ZEN, actual_read)
+
+        with MockFile(ZEN) as file:
+            actual_read = file.read(-1)
+        self.assertEqual(ZEN, actual_read)
+        self.test_passed = True
+
+    def test_read_multiple(self):
+        actual_read = b""
+        with MockFile(ZEN) as file:
+            for _ in range(5):
+                actual_read += file.read(30)
+        self.assertEqual(ZEN[:150], actual_read)
+        self.test_passed = True
+
+    def test_read_multiple_chunk(self):
+        actual_read = b""
+        with MockFile(ZEN, chunk=20) as file:
+            for _ in range(5):
+                actual_read += file.read()
+            actual_read += file.read(10)
+        self.assertEqual(ZEN[:110], actual_read)
+
+        actual_read = b""
+        with MockFile(ZEN, chunk=20) as file:
+            for size in [None, -2, -1, 0, 1, 2]:
+                actual_read += file.read(size)
+            actual_read += file.read(10)
+        self.assertEqual(ZEN[:73], actual_read)
+        self.test_passed = True
+
+    def test_read_under_size(self):
+        with MockFile(ZEN, chunk=257) as file:
+            actual_read = file.read()
+        self.assertEqual(ZEN[:257], actual_read)
+        self.test_passed = True
+
+    def test_write_once(self):
+        with MockFile(chunk=257) as file:
+            write_len = file.write(ZEN)
+            self.assertEqual(ZEN[:257], file.getbuffer())
+            self.assertEqual(257, write_len)
+        self.test_passed = True
+
+    def test_write_multiple(self):
+        write_len = 0
+        expected = b"Tomorrow's victory is today's practice."
+        with MockFile() as file:
+            write_len += file.write(b"Tomorro")
+            write_len += file.write(b"w's victo")
+            write_len += file.write(b"ry is today")
+            write_len += file.write(b"'s practice.")
+            self.assertEqual(expected, file.getbuffer())
+            self.assertEqual(len(expected), write_len)
+        self.test_passed = True
+
+    def test_write_under_size(self):
+        with MockFile(chunk=257) as file:
+            write_len = file.write(ZEN[:123])
+            self.assertEqual(ZEN[:123], file.getbuffer())
+            self.assertEqual(123, write_len)
+        self.test_passed = True
+
+
+# Tests for paasio.py begin here
+
+
+class MeteredSocketTest(unittest.TestCase):
+    def test_context_manager(self):
+        wrapped = MockSock()
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        mock.__exit__.side_effect = wrapped.__exit__
+        with MeteredSocket(mock) as socket:
+            self.assertFalse(mock.__enter__.called)
+            socket.recv(30)
+        self.assertFalse(mock.__enter__.called)
+        mock.__exit__.assert_called_once_with(None, None, None)
+        self.assertEqual(2, len(mock.mock_calls))
+        with self.assertRaisesRegex(OSError, os.strerror(errno.EBADF)):
+            socket.recv(30)
+        with self.assertRaisesRegex(OSError, os.strerror(errno.EBADF)):
+            socket.send(b"")
+
+    def test_context_manager_exception_raise(self):
+        exception = MockException("Should raise")
+        wrapped = MockSock(exception=exception)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        mock.__exit__.side_effect = wrapped.__exit__
+        with self.assertRaisesRegex(MockException, "Should raise") as err:
+            with MeteredSocket(mock) as socket:
+                self.assertFalse(mock.__enter__.called)
+                socket.recv(4096)
+        self.assertFalse(mock.__enter__.called)
+        mock.__exit__.assert_called_once_with(
+            MockException, err.exception, ANY,
+        )
+        self.assertEqual(exception, err.exception)
+
+    def test_context_manager_exception_suppress(self):
+        exception = MockException("Should suppress")
+        wrapped = MockSock(exception=exception)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        mock.__exit__.side_effect = wrapped.__exit__
+        with MeteredSocket(mock) as socket:
+            self.assertFalse(mock.__enter__.called)
+            socket.recv(4096)
+        self.assertFalse(mock.__enter__.called)
+        mock.__exit__.assert_called_once_with(
+            MockException, exception, ANY,
+        )
+
+    def test_recv_once(self):
+        mock = NonCallableMagicMock(wraps=MockSock(), autospec=True)
+        with MeteredSocket(mock) as socket:
+            actual_recv = socket.recv(4096)
+        self.assertEqual(ZEN, actual_recv)
+        self.assertEqual(1, socket.recv_ops)
+        self.assertEqual(len(ZEN), socket.recv_bytes)
+        self.assertEqual(1, mock.recv.call_count)
+
+    def test_recv_multiple(self):
+        wrapped = MockSock()
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        actual_recv = b""
+        with MeteredSocket(mock) as socket:
+            for _ in range(5):
+                actual_recv += socket.recv(30)
+        self.assertEqual(ZEN[:150], actual_recv)
+        self.assertEqual(5, socket.recv_ops)
+        self.assertEqual(150, socket.recv_bytes)
+        self.assertEqual(5, mock.recv.call_count)
+
+    def test_recv_multiple_chunk(self):
+        wrapped = MockSock(chunk=20)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        actual_recv = b""
+        with MeteredSocket(mock) as socket:
+            for _ in range(5):
+                actual_recv += socket.recv(4096)
+            actual_recv += socket.recv(10)
+        self.assertEqual(ZEN[:110], actual_recv)
+        self.assertEqual(6, socket.recv_ops)
+        self.assertEqual(110, socket.recv_bytes)
+        self.assertEqual(6, mock.recv.call_count)
+
+    def test_recv_under_size(self):
+        wrapped = MockSock(chunk=257)  # largish odd number
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        with MeteredSocket(mock) as socket:
+            actual_recv = socket.recv(4096)
+        self.assertEqual(ZEN[:257], actual_recv)
+        self.assertEqual(1, socket.recv_ops)
+        self.assertEqual(257, socket.recv_bytes)
+        self.assertEqual(1, mock.recv.call_count)
+
+    def test_send_once(self):
+        wrapped = MockSock(chunk=257)  # largish odd number
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        with MeteredSocket(mock) as socket:
+            send_len = socket.send(ZEN)
+            self.assertEqual(ZEN[:257], wrapped._sender.getbuffer())
+        self.assertEqual(257, send_len)
+        self.assertEqual(1, socket.send_ops)
+        self.assertEqual(257, socket.send_bytes)
+        self.assertEqual(1, mock.send.call_count)
+
+    def test_send_multiple(self):
+        wrapped = MockSock()
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        send_len = 0
+        expected = b"Tomorrow's victory is today's practice."
+        with MeteredSocket(mock) as socket:
+            send_len += socket.send(b"Tomorro")
+            send_len += socket.send(b"w's victo")
+            send_len += socket.send(b"ry is today")
+            send_len += socket.send(b"'s practice.")
+            self.assertEqual(expected, wrapped._sender.getbuffer())
+        self.assertEqual(39, send_len)
+        self.assertEqual(4, socket.send_ops)
+        self.assertEqual(39, socket.send_bytes)
+        self.assertEqual(4, mock.send.call_count)
+
+    def test_send_under_size(self):
+        wrapped = MockSock(chunk=257)  # largish odd number
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        with MeteredSocket(mock) as socket:
+            send_len = socket.send(ZEN[:123])
+            self.assertEqual(ZEN[:123], wrapped._sender.getbuffer())
+        self.assertEqual(123, send_len)
+        self.assertEqual(1, socket.send_ops)
+        self.assertEqual(123, socket.send_bytes)
+        self.assertEqual(1, mock.send.call_count)
+
+    def test_bufsize_required(self):
+        mock = NonCallableMagicMock(wraps=MockSock(), autospec=True)
+        with self.assertRaisesRegex(TypeError, "argument"):
+            with MeteredSocket(mock) as socket:
+                socket.recv()
+        self.assertFalse(mock.recv.called)
+
+        mock = NonCallableMagicMock(wraps=MockSock(), autospec=True)
+        with self.assertRaisesRegex(TypeError, "^'NoneType'.+integer$"):
+            with MeteredSocket(mock) as socket:
+                socket.recv(None)
+        self.assertTrue(
+            call(None) in mock.recv.mock_calls
+            or call(None, ANY) in mock.recv.mock_calls
+        )
+
+    def test_flags_support(self):
+        mock = NonCallableMagicMock(wraps=MockSock(), autospec=True)
+        with MeteredSocket(mock) as socket:
+            self.assertEqual(len(ZEN), socket.send(ZEN, 42))
+            self.assertEqual(ZEN, socket.recv(4096, 24))
+        mock.send.assert_called_once_with(ZEN, 42)
+        mock.recv.assert_called_once_with(4096, 24)
+
+        wrapped = MockSock()
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        with MeteredSocket(mock) as socket:
+            socket.recv(50)
+            self.assertEqual(0, wrapped.flags)
+            socket.send(b"no flags")
+            self.assertEqual(0, wrapped.flags)
+            socket.recv(30, 30)
+            self.assertEqual(30, wrapped.flags)
+            socket.send(b"flags", 1024)
+            self.assertEqual(1024, wrapped.flags)
+            with self.assertRaisesRegex(TypeError, "integer is required"):
+                socket.send(b"data", None)
+            with self.assertRaisesRegex(TypeError, "integer is required"):
+                socket.send(b"data", b"flags")
+            with self.assertRaisesRegex(TypeError, "integer is required"):
+                socket.recv(b"data", None)
+            with self.assertRaisesRegex(TypeError, "integer is required"):
+                socket.recv(b"data", b"flags")
+
+    def test_stats_read_only(self):
+        mock = NonCallableMagicMock(wraps=MockSock(), autospec=True)
+        with MeteredSocket(mock) as socket:
+            self.assertEqual(0, socket.send_ops)
+            self.assertEqual(0, socket.send_bytes)
+            self.assertEqual(0, socket.recv_ops)
+            self.assertEqual(0, socket.recv_bytes)
+            for _ in range(277):
+                socket.send(b"b")
+            socket.send(b"bytes")
+            for _ in range(257):
+                socket.recv(1)
+            socket.recv(2)
+            self.assertEqual(278, socket.send_ops)
+            self.assertEqual(282, socket.send_bytes)
+            self.assertEqual(258, socket.recv_ops)
+            self.assertEqual(259, socket.recv_bytes)
+            with self.assertRaisesRegex(AttributeError, "can't set"):
+                socket.send_ops = 0
+            with self.assertRaisesRegex(AttributeError, "can't set"):
+                socket.send_bytes = 0
+            with self.assertRaisesRegex(AttributeError, "can't set"):
+                socket.recv_ops = 0
+            with self.assertRaisesRegex(AttributeError, "can't set"):
+                socket.recv_bytes = 0
+            self.assertEqual(278, socket.send_ops)
+            self.assertEqual(282, socket.send_bytes)
+            self.assertEqual(258, socket.recv_ops)
+            self.assertEqual(259, socket.recv_bytes)
+
+
+@patch("paasio.super", create=True, new_callable=SuperMock)
+class MeteredFileTest(unittest.TestCase):
+    def test_context_manager(self, super_mock):
+        wrapped = MockFile(ZEN)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        mock.__exit__.side_effect = wrapped.__exit__
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            self.assertEqual(1, super_mock.init_called)
+            self.assertFalse(mock.__enter__.called)
+            file.read()
+        self.assertFalse(mock.__enter__.called)
+        mock.__exit__.assert_called_once_with(None, None, None)
+        self.assertEqual(2, len(mock.mock_calls))
+        with self.assertRaisesRegex(
+            ValueError, "I/O operation on closed file."
+        ):
+            file.read()
+        with self.assertRaisesRegex(
+            ValueError, "I/O operation on closed file."
+        ):
+            file.write(b"data")
+
+    def test_context_manager_exception_raise(self, super_mock):
+        exception = MockException("Should raise")
+        wrapped = MockFile(ZEN, exception=exception)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        mock.__exit__.side_effect = wrapped.__exit__
+        super_mock.mock_object = mock
+        with self.assertRaisesRegex(MockException, "Should raise") as err:
+            with MeteredFile() as file:
+                self.assertFalse(mock.__enter__.called)
+                file.read()
+        self.assertFalse(mock.__enter__.called)
+        mock.__exit__.assert_called_once_with(
+            MockException, err.exception, ANY,
+        )
+        self.assertEqual(exception, err.exception)
+
+    def test_context_manager_exception_suppress(self, super_mock):
+        exception = MockException("Should suppress")
+        wrapped = MockFile(ZEN, exception=exception)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        mock.__exit__.side_effect = wrapped.__exit__
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            self.assertFalse(mock.__enter__.called)
+            file.read()
+        self.assertFalse(mock.__enter__.called)
+        mock.__exit__.assert_called_once_with(
+            MockException, exception, ANY,
+        )
+
+    def test_iteration(self, super_mock):
+        mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True)
+        super_mock.mock_object = mock
+        actual_reads = b""
+        file = MeteredFile()
+        for line in file:
+            actual_reads += line
+            self.assertLess(
+                0, mock.readline.call_count, "File's readline not called"
+            )
+            self.assertGreater(
+                50, mock.readline.call_count, "Possible infinte loop detected"
+            )
+            self.assertEqual(file.read_ops, mock.readline.call_count)
+        self.assertFalse(mock.__iter__.called)
+        self.assertEqual(len(ZEN), file.read_bytes)
+        self.assertEqual(ZEN, actual_reads)
+
+    def test_read_once(self, super_mock):
+        mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True)
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            actual_read = file.read()
+        self.assertEqual(ZEN, actual_read)
+        self.assertEqual((len(ZEN)), file.read_bytes)
+        self.assertEqual(1, file.read_ops)
+        self.assertEqual(mock.read.call_count, file.read_ops)
+
+        mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True)
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            actual_read = file.read(None)
+        self.assertEqual(ZEN, actual_read)
+        self.assertEqual((len(ZEN)), file.read_bytes)
+        self.assertEqual(1, file.read_ops)
+        self.assertEqual(mock.read.call_count, file.read_ops)
+
+        mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True)
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            actual_read = file.read(-1)
+        self.assertEqual(ZEN, actual_read)
+        self.assertEqual((len(ZEN)), file.read_bytes)
+        self.assertEqual(1, file.read_ops)
+        self.assertEqual(mock.read.call_count, file.read_ops)
+
+    def test_read_multiple(self, super_mock):
+        wrapped = MockFile(ZEN)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        super_mock.mock_object = mock
+        actual_read = b""
+        with MeteredFile() as file:
+            for _ in range(5):
+                actual_read += file.read(30)
+        self.assertEqual(ZEN[:150], actual_read)
+        self.assertEqual(5, file.read_ops)
+        self.assertEqual(150, file.read_bytes)
+        self.assertEqual(5, mock.read.call_count)
+
+    def test_read_multiple_chunk(self, super_mock):
+        wrapped = MockFile(ZEN, chunk=20)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        super_mock.mock_object = mock
+        actual_read = b""
+        with MeteredFile() as file:
+            for _ in range(5):
+                actual_read += file.read()
+            actual_read += file.read(10)
+        self.assertEqual(ZEN[:110], actual_read)
+        self.assertEqual(6, file.read_ops)
+        self.assertEqual(110, file.read_bytes)
+        self.assertEqual(6, mock.read.call_count)
+
+        wrapped = MockFile(ZEN, chunk=20)
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        super_mock.mock_object = mock
+        actual_read = b""
+        with MeteredFile() as file:
+            for size in [None, -2, -1, 0, 1, 2]:
+                actual_read += file.read(size)
+            actual_read += file.read(10)
+        self.assertEqual(ZEN[:73], actual_read)
+        self.assertEqual(7, file.read_ops)
+        self.assertEqual(73, file.read_bytes)
+        self.assertEqual(7, mock.read.call_count)
+
+    def test_read_under_size(self, super_mock):
+        wrapped = MockFile(ZEN, chunk=257)  # largish odd number
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            actual_read = file.read()
+        self.assertEqual(ZEN[:257], actual_read)
+        self.assertEqual(1, file.read_ops)
+        self.assertEqual(257, file.read_bytes)
+        self.assertEqual(1, mock.read.call_count)
+
+    def test_write_once(self, super_mock):
+        wrapped = MockFile(chunk=257)  # largish odd number
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            write_len = file.write(ZEN)
+            self.assertEqual(ZEN[:257], wrapped.getbuffer())
+        self.assertEqual(257, write_len)
+        self.assertEqual(1, file.write_ops)
+        self.assertEqual(257, file.write_bytes)
+        self.assertEqual(1, mock.write.call_count)
+
+    def test_write_multiple(self, super_mock):
+        wrapped = MockFile()
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        super_mock.mock_object = mock
+        write_len = 0
+        expected = b"Tomorrow's victory is today's practice."
+        with MeteredFile() as file:
+            write_len += file.write(b"Tomorro")
+            write_len += file.write(b"w's victo")
+            write_len += file.write(b"ry is today")
+            write_len += file.write(b"'s practice.")
+            self.assertEqual(expected, wrapped.getbuffer())
+        self.assertEqual(39, write_len)
+        self.assertEqual(4, file.write_ops)
+        self.assertEqual(39, file.write_bytes)
+        self.assertEqual(4, mock.write.call_count)
+
+    def test_write_under_size(self, super_mock):
+        wrapped = MockFile(chunk=257)  # largish odd number
+        mock = NonCallableMagicMock(wraps=wrapped, autospec=True)
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            write_len = file.write(ZEN[:123])
+            self.assertEqual(ZEN[:123], wrapped.getbuffer())
+        self.assertEqual(123, write_len)
+        self.assertEqual(1, file.write_ops)
+        self.assertEqual(123, file.write_bytes)
+        self.assertEqual(1, mock.write.call_count)
+
+    def test_stats_read_only(self, super_mock):
+        mock = NonCallableMagicMock(wraps=MockFile(ZEN), autospec=True)
+        super_mock.mock_object = mock
+        with MeteredFile() as file:
+            self.assertEqual(0, file.read_ops)
+            self.assertEqual(0, file.read_bytes)
+            for _ in range(57):
+                file.read(1)
+            file.read(2)
+            self.assertEqual(58, file.read_ops)
+            self.assertEqual(59, file.read_bytes)
+            self.assertEqual(0, file.write_ops)
+            self.assertEqual(0, file.write_bytes)
+            for _ in range(77):
+                file.write(b"b")
+            file.write(b"bytes")
+            self.assertEqual(78, file.write_ops)
+            self.assertEqual(82, file.write_bytes)
+            with self.assertRaisesRegex(AttributeError, "can't set"):
+                file.write_ops = 0
+            with self.assertRaisesRegex(AttributeError, "can't set"):
+                file.write_bytes = 0
+            with self.assertRaisesRegex(AttributeError, "can't set"):
+                file.read_ops = 0
+            with self.assertRaisesRegex(AttributeError, "can't set"):
+                file.read_bytes = 0
+            self.assertEqual(78, file.write_ops)
+            self.assertEqual(82, file.write_bytes)
+            self.assertEqual(58, file.read_ops)
+            self.assertEqual(59, file.read_bytes)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/exercises/palindrome-products/.meta/tests.toml b/exercises/palindrome-products/.meta/tests.toml
new file mode 100644
index 00000000..54b7736e
--- /dev/null
+++ b/exercises/palindrome-products/.meta/tests.toml
@@ -0,0 +1,37 @@
+[canonical-tests]
+
+# finds the smallest palindrome from single digit factors
+"5cff78fe-cf02-459d-85c2-ce584679f887" = true
+
+# finds the largest palindrome from single digit factors
+"0853f82c-5fc4-44ae-be38-fadb2cced92d" = true
+
+# find the smallest palindrome from double digit factors
+"66c3b496-bdec-4103-9129-3fcb5a9063e1" = true
+
+# find the largest palindrome from double digit factors
+"a10682ae-530a-4e56-b89d-69664feafe53" = true
+
+# find smallest palindrome from triple digit factors
+"cecb5a35-46d1-4666-9719-fa2c3af7499d" = true
+
+# find the largest palindrome from triple digit factors
+"edab43e1-c35f-4ea3-8c55-2f31dddd92e5" = true
+
+# find smallest palindrome from four digit factors
+"4f802b5a-9d74-4026-a70f-b53ff9234e4e" = true
+
+# find the largest palindrome from four digit factors
+"787525e0-a5f9-40f3-8cb2-23b52cf5d0be" = true
+
+# empty result for smallest if no palindrome in the range
+"58fb1d63-fddb-4409-ab84-a7a8e58d9ea0" = true
+
+# empty result for largest if no palindrome in the range
+"9de9e9da-f1d9-49a5-8bfc-3d322efbdd02" = true
+
+# error result for smallest if min is more than max
+"12e73aac-d7ee-4877-b8aa-2aa3dcdb9f8a" = true
+
+# error result for largest if min is more than max
+"eeeb5bff-3f47-4b1e-892f-05829277bd74" = true
diff --git a/exercises/pangram/.meta/tests.toml b/exercises/pangram/.meta/tests.toml
new file mode 100644
index 00000000..75fba6dd
--- /dev/null
+++ b/exercises/pangram/.meta/tests.toml
@@ -0,0 +1,31 @@
+[canonical-tests]
+
+# empty sentence
+"64f61791-508e-4f5c-83ab-05de042b0149" = true
+
+# perfect lower case
+"74858f80-4a4d-478b-8a5e-c6477e4e4e84" = true
+
+# only lower case
+"61288860-35ca-4abe-ba08-f5df76ecbdcd" = true
+
+# missing the letter 'x'
+"6564267d-8ac5-4d29-baf2-e7d2e304a743" = true
+
+# missing the letter 'h'
+"c79af1be-d715-4cdb-a5f2-b2fa3e7e0de0" = true
+
+# with underscores
+"d835ec38-bc8f-48e4-9e36-eb232427b1df" = true
+
+# with numbers
+"8cc1e080-a178-4494-b4b3-06982c9be2a8" = true
+
+# missing letters replaced by numbers
+"bed96b1c-ff95-45b8-9731-fdbdcb6ede9a" = true
+
+# mixed case and punctuation
+"938bd5d8-ade5-40e2-a2d9-55a338a01030" = true
+
+# case insensitive
+"2577bf54-83c8-402d-a64b-a2c0f7bb213a" = true
diff --git a/exercises/pascals-triangle/.meta/tests.toml b/exercises/pascals-triangle/.meta/tests.toml
new file mode 100644
index 00000000..879944db
--- /dev/null
+++ b/exercises/pascals-triangle/.meta/tests.toml
@@ -0,0 +1,25 @@
+[canonical-tests]
+
+# zero rows
+"9920ce55-9629-46d5-85d6-4201f4a4234d" = true
+
+# single row
+"70d643ce-a46d-4e93-af58-12d88dd01f21" = true
+
+# two rows
+"a6e5a2a2-fc9a-4b47-9f4f-ed9ad9fbe4bd" = true
+
+# three rows
+"97206a99-79ba-4b04-b1c5-3c0fa1e16925" = true
+
+# four rows
+"565a0431-c797-417c-a2c8-2935e01ce306" = true
+
+# five rows
+"06f9ea50-9f51-4eb2-b9a9-c00975686c27" = true
+
+# six rows
+"c3912965-ddb4-46a9-848e-3363e6b00b13" = true
+
+# ten rows
+"6cb26c66-7b57-4161-962c-81ec8c99f16b" = true
diff --git a/exercises/perfect-numbers/.meta/tests.toml b/exercises/perfect-numbers/.meta/tests.toml
new file mode 100644
index 00000000..af4b07b1
--- /dev/null
+++ b/exercises/perfect-numbers/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# Smallest perfect number is classified correctly
+"163e8e86-7bfd-4ee2-bd68-d083dc3381a3" = true
+
+# Medium perfect number is classified correctly
+"169a7854-0431-4ae0-9815-c3b6d967436d" = true
+
+# Large perfect number is classified correctly
+"ee3627c4-7b36-4245-ba7c-8727d585f402" = true
+
+# Smallest abundant number is classified correctly
+"80ef7cf8-9ea8-49b9-8b2d-d9cb3db3ed7e" = true
+
+# Medium abundant number is classified correctly
+"3e300e0d-1a12-4f11-8c48-d1027165ab60" = true
+
+# Large abundant number is classified correctly
+"ec7792e6-8786-449c-b005-ce6dd89a772b" = true
+
+# Smallest prime deficient number is classified correctly
+"e610fdc7-2b6e-43c3-a51c-b70fb37413ba" = true
+
+# Smallest non-prime deficient number is classified correctly
+"0beb7f66-753a-443f-8075-ad7fbd9018f3" = true
+
+# Medium deficient number is classified correctly
+"1c802e45-b4c6-4962-93d7-1cad245821ef" = true
+
+# Large deficient number is classified correctly
+"47dd569f-9e5a-4a11-9a47-a4e91c8c28aa" = true
+
+# Edge case (no factors other than itself) is classified correctly
+"a696dec8-6147-4d68-afad-d38de5476a56" = true
+
+# Zero is rejected (not a natural number)
+"72445cee-660c-4d75-8506-6c40089dc302" = true
+
+# Negative integer is rejected (not a natural number)
+"2d72ce2c-6802-49ac-8ece-c790ba3dae13" = true
diff --git a/exercises/phone-number/.meta/tests.toml b/exercises/phone-number/.meta/tests.toml
new file mode 100644
index 00000000..0c914d43
--- /dev/null
+++ b/exercises/phone-number/.meta/tests.toml
@@ -0,0 +1,55 @@
+[canonical-tests]
+
+# cleans the number
+"79666dce-e0f1-46de-95a1-563802913c35" = true
+
+# cleans numbers with dots
+"c360451f-549f-43e4-8aba-fdf6cb0bf83f" = true
+
+# cleans numbers with multiple spaces
+"08f94c34-9a37-46a2-a123-2a8e9727395d" = true
+
+# invalid when 9 digits
+"598d8432-0659-4019-a78b-1c6a73691d21" = true
+
+# invalid when 11 digits does not start with a 1
+"57061c72-07b5-431f-9766-d97da7c4399d" = true
+
+# valid when 11 digits and starting with 1
+"9962cbf3-97bb-4118-ba9b-38ff49c64430" = true
+
+# valid when 11 digits and starting with 1 even with punctuation
+"fa724fbf-054c-4d91-95da-f65ab5b6dbca" = true
+
+# invalid when more than 11 digits
+"c6a5f007-895a-4fc5-90bc-a7e70f9b5cad" = true
+
+# invalid with letters
+"63f38f37-53f6-4a5f-bd86-e9b404f10a60" = true
+
+# invalid with punctuations
+"4bd97d90-52fd-45d3-b0db-06ab95b1244e" = true
+
+# invalid if area code starts with 0
+"d77d07f8-873c-4b17-8978-5f66139bf7d7" = true
+
+# invalid if area code starts with 1
+"c7485cfb-1e7b-4081-8e96-8cdb3b77f15e" = true
+
+# invalid if exchange code starts with 0
+"4d622293-6976-413d-b8bf-dd8a94d4e2ac" = true
+
+# invalid if exchange code starts with 1
+"4cef57b4-7d8e-43aa-8328-1e1b89001262" = true
+
+# invalid if area code starts with 0 on valid 11-digit number
+"9925b09c-1a0d-4960-a197-5d163cbe308c" = true
+
+# invalid if area code starts with 1 on valid 11-digit number
+"3f809d37-40f3-44b5-ad90-535838b1a816" = true
+
+# invalid if exchange code starts with 0 on valid 11-digit number
+"e08e5532-d621-40d4-b0cc-96c159276b65" = true
+
+# invalid if exchange code starts with 1 on valid 11-digit number
+"57b32f3d-696a-455c-8bf1-137b6d171cdf" = true
diff --git a/exercises/pig-latin/.meta/tests.toml b/exercises/pig-latin/.meta/tests.toml
new file mode 100644
index 00000000..b53c8851
--- /dev/null
+++ b/exercises/pig-latin/.meta/tests.toml
@@ -0,0 +1,67 @@
+[canonical-tests]
+
+# word beginning with a
+"11567f84-e8c6-4918-aedb-435f0b73db57" = true
+
+# word beginning with e
+"f623f581-bc59-4f45-9032-90c3ca9d2d90" = true
+
+# word beginning with i
+"7dcb08b3-23a6-4e8a-b9aa-d4e859450d58" = true
+
+# word beginning with o
+"0e5c3bff-266d-41c8-909f-364e4d16e09c" = true
+
+# word beginning with u
+"614ba363-ca3c-4e96-ab09-c7320799723c" = true
+
+# word beginning with a vowel and followed by a qu
+"bf2538c6-69eb-4fa7-a494-5a3fec911326" = true
+
+# word beginning with p
+"e5be8a01-2d8a-45eb-abb4-3fcc9582a303" = true
+
+# word beginning with k
+"d36d1e13-a7ed-464d-a282-8820cb2261ce" = true
+
+# word beginning with x
+"d838b56f-0a89-4c90-b326-f16ff4e1dddc" = true
+
+# word beginning with q without a following u
+"bce94a7a-a94e-4e2b-80f4-b2bb02e40f71" = true
+
+# word beginning with ch
+"c01e049a-e3e2-451c-bf8e-e2abb7e438b8" = true
+
+# word beginning with qu
+"9ba1669e-c43f-4b93-837a-cfc731fd1425" = true
+
+# word beginning with qu and a preceding consonant
+"92e82277-d5e4-43d7-8dd3-3a3b316c41f7" = true
+
+# word beginning with th
+"79ae4248-3499-4d5b-af46-5cb05fa073ac" = true
+
+# word beginning with thr
+"e0b3ae65-f508-4de3-8999-19c2f8e243e1" = true
+
+# word beginning with sch
+"20bc19f9-5a35-4341-9d69-1627d6ee6b43" = true
+
+# word beginning with yt
+"54b796cb-613d-4509-8c82-8fbf8fc0af9e" = true
+
+# word beginning with xr
+"8c37c5e1-872e-4630-ba6e-d20a959b67f6" = true
+
+# y is treated like a consonant at the beginning of a word
+"a4a36d33-96f3-422c-a233-d4021460ff00" = true
+
+# y is treated like a vowel at the end of a consonant cluster
+"adc90017-1a12-4100-b595-e346105042c7" = true
+
+# y as second letter in two letter word
+"29b4ca3d-efe5-4a95-9a54-8467f2e5e59a" = true
+
+# a whole phrase
+"44616581-5ce3-4a81-82d0-40c7ab13d2cf" = true
diff --git a/exercises/poker/.meta/tests.toml b/exercises/poker/.meta/tests.toml
new file mode 100644
index 00000000..b5c223c8
--- /dev/null
+++ b/exercises/poker/.meta/tests.toml
@@ -0,0 +1,85 @@
+[canonical-tests]
+
+# single hand always wins
+"161f485e-39c2-4012-84cf-bec0c755b66c" = true
+
+# highest card out of all hands wins
+"370ac23a-a00f-48a9-9965-6f3fb595cf45" = true
+
+# a tie has multiple winners
+"d94ad5a7-17df-484b-9932-c64fc26cff52" = true
+
+# multiple hands with the same high cards, tie compares next highest ranked, down to last card
+"61ed83a9-cfaa-40a5-942a-51f52f0a8725" = true
+
+# one pair beats high card
+"f7175a89-34ff-44de-b3d7-f6fd97d1fca4" = true
+
+# highest pair wins
+"e114fd41-a301-4111-a9e7-5a7f72a76561" = true
+
+# two pairs beats one pair
+"935bb4dc-a622-4400-97fa-86e7d06b1f76" = true
+
+# both hands have two pairs, highest ranked pair wins
+"c8aeafe1-6e3d-4711-a6de-5161deca91fd" = true
+
+# both hands have two pairs, with the same highest ranked pair, tie goes to low pair
+"88abe1ba-7ad7-40f3-847e-0a26f8e46a60" = true
+
+# both hands have two identically ranked pairs, tie goes to remaining card (kicker)
+"15a7a315-0577-47a3-9981-d6cf8e6f387b" = true
+
+# three of a kind beats two pair
+"21e9f1e6-2d72-49a1-a930-228e5e0195dc" = true
+
+# both hands have three of a kind, tie goes to highest ranked triplet
+"c2fffd1f-c287-480f-bf2d-9628e63bbcc3" = true
+
+# with multiple decks, two players can have same three of a kind, ties go to highest remaining cards
+"eb856cc2-481c-4b0d-9835-4d75d07a5d9d" = true
+
+# a straight beats three of a kind
+"a858c5d9-2f28-48e7-9980-b7fa04060a60" = true
+
+# aces can end a straight (10 J Q K A)
+"73c9c756-e63e-4b01-a88d-0d4491a7a0e3" = true
+
+# aces can start a straight (A 2 3 4 5)
+"76856b0d-35cd-49ce-a492-fe5db53abc02" = true
+
+# both hands with a straight, tie goes to highest ranked card
+"6980c612-bbff-4914-b17a-b044e4e69ea1" = true
+
+# even though an ace is usually high, a 5-high straight is the lowest-scoring straight
+"5135675c-c2fc-4e21-9ba3-af77a32e9ba4" = true
+
+# flush beats a straight
+"c601b5e6-e1df-4ade-b444-b60ce13b2571" = true
+
+# both hands have a flush, tie goes to high card, down to the last one if necessary
+"4d90261d-251c-49bd-a468-896bf10133de" = true
+
+# full house beats a flush
+"3a19361d-8974-455c-82e5-f7152f5dba7c" = true
+
+# both hands have a full house, tie goes to highest-ranked triplet
+"eb73d0e6-b66c-4f0f-b8ba-bf96bc0a67f0" = true
+
+# with multiple decks, both hands have a full house with the same triplet, tie goes to the pair
+"34b51168-1e43-4c0d-9b32-e356159b4d5d" = true
+
+# four of a kind beats a full house
+"d61e9e99-883b-4f99-b021-18f0ae50c5f4" = true
+
+# both hands have four of a kind, tie goes to high quad
+"2e1c8c63-e0cb-4214-a01b-91954490d2fe" = true
+
+# with multiple decks, both hands with identical four of a kind, tie determined by kicker
+"892ca75d-5474-495d-9f64-a6ce2dcdb7e1" = true
+
+# straight flush beats four of a kind
+"923bd910-dc7b-4f7d-a330-8b42ec10a3ac" = true
+
+# both hands have straight flush, tie goes to highest-ranked card
+"d0927f70-5aec-43db-aed8-1cbd1b6ee9ad" = true
diff --git a/exercises/pov/.meta/tests.toml b/exercises/pov/.meta/tests.toml
new file mode 100644
index 00000000..5995f3f5
--- /dev/null
+++ b/exercises/pov/.meta/tests.toml
@@ -0,0 +1,46 @@
+[canonical-tests]
+
+# Results in the same tree if the input tree is a singleton
+"1b3cd134-49ad-4a7d-8376-7087b7e70792" = true
+
+# Can reroot a tree with a parent and one sibling
+"0778c745-0636-40de-9edd-25a8f40426f6" = true
+
+# Can reroot a tree with a parent and many siblings
+"fdfdef0a-4472-4248-8bcf-19cf33f9c06e" = true
+
+# Can reroot a tree with new root deeply nested in tree
+"cbcf52db-8667-43d8-a766-5d80cb41b4bb" = true
+
+# Moves children of the new root to same level as former parent
+"e27fa4fa-648d-44cd-90af-d64a13d95e06" = true
+
+# Can reroot a complex tree with cousins
+"09236c7f-7c83-42cc-87a1-25afa60454a3" = true
+
+# Errors if target does not exist in a singleton tree
+"f41d5eeb-8973-448f-a3b0-cc1e019a4193" = true
+
+# Errors if target does not exist in a large tree
+"9dc0a8b3-df02-4267-9a41-693b6aff75e7" = true
+
+# Can find path to parent
+"02d1f1d9-428d-4395-b026-2db35ffa8f0a" = true
+
+# Can find path to sibling
+"d0002674-fcfb-4cdc-9efa-bfc54e3c31b5" = true
+
+# Can find path to cousin
+"c9877cd1-0a69-40d4-b362-725763a5c38f" = true
+
+# Can find path not involving root
+"9fb17a82-2c14-4261-baa3-2f3f234ffa03" = true
+
+# Can find path from nodes other than x
+"5124ed49-7845-46ad-bc32-97d5ac7451b2" = true
+
+# Errors if destination does not exist
+"f52a183c-25cc-4c87-9fc9-0e7f81a5725c" = true
+
+# Errors if source does not exist
+"f4fe18b9-b4a2-4bd5-a694-e179155c2149" = true
diff --git a/exercises/prime-factors/.meta/tests.toml b/exercises/prime-factors/.meta/tests.toml
new file mode 100644
index 00000000..ba44aa8f
--- /dev/null
+++ b/exercises/prime-factors/.meta/tests.toml
@@ -0,0 +1,22 @@
+[canonical-tests]
+
+# no factors
+"924fc966-a8f5-4288-82f2-6b9224819ccd" = true
+
+# prime number
+"17e30670-b105-4305-af53-ddde182cb6ad" = true
+
+# square of a prime
+"f59b8350-a180-495a-8fb1-1712fbee1158" = true
+
+# cube of a prime
+"bc8c113f-9580-4516-8669-c5fc29512ceb" = true
+
+# product of primes and non-primes
+"00485cd3-a3fe-4fbe-a64a-a4308fc1f870" = true
+
+# product of primes
+"02251d54-3ca1-4a9b-85e1-b38f4b0ccb91" = true
+
+# factors include a large prime
+"070cf8dc-e202-4285-aa37-8d775c9cd473" = true
diff --git a/exercises/protein-translation/.meta/tests.toml b/exercises/protein-translation/.meta/tests.toml
new file mode 100644
index 00000000..b626eb63
--- /dev/null
+++ b/exercises/protein-translation/.meta/tests.toml
@@ -0,0 +1,70 @@
+[canonical-tests]
+
+# Methionine RNA sequence
+"96d3d44f-34a2-4db4-84cd-fff523e069be" = true
+
+# Phenylalanine RNA sequence 1
+"1b4c56d8-d69f-44eb-be0e-7b17546143d9" = true
+
+# Phenylalanine RNA sequence 2
+"81b53646-bd57-4732-b2cb-6b1880e36d11" = true
+
+# Leucine RNA sequence 1
+"42f69d4f-19d2-4d2c-a8b0-f0ae9ee1b6b4" = true
+
+# Leucine RNA sequence 2
+"ac5edadd-08ed-40a3-b2b9-d82bb50424c4" = true
+
+# Serine RNA sequence 1
+"8bc36e22-f984-44c3-9f6b-ee5d4e73f120" = true
+
+# Serine RNA sequence 2
+"5c3fa5da-4268-44e5-9f4b-f016ccf90131" = true
+
+# Serine RNA sequence 3
+"00579891-b594-42b4-96dc-7ff8bf519606" = true
+
+# Serine RNA sequence 4
+"08c61c3b-fa34-4950-8c4a-133945570ef6" = true
+
+# Tyrosine RNA sequence 1
+"54e1e7d8-63c0-456d-91d2-062c72f8eef5" = true
+
+# Tyrosine RNA sequence 2
+"47bcfba2-9d72-46ad-bbce-22f7666b7eb1" = true
+
+# Cysteine RNA sequence 1
+"3a691829-fe72-43a7-8c8e-1bd083163f72" = true
+
+# Cysteine RNA sequence 2
+"1b6f8a26-ca2f-43b8-8262-3ee446021767" = true
+
+# Tryptophan RNA sequence
+"1e91c1eb-02c0-48a0-9e35-168ad0cb5f39" = true
+
+# STOP codon RNA sequence 1
+"e547af0b-aeab-49c7-9f13-801773a73557" = true
+
+# STOP codon RNA sequence 2
+"67640947-ff02-4f23-a2ef-816f8a2ba72e" = true
+
+# STOP codon RNA sequence 3
+"9c2ad527-ebc9-4ace-808b-2b6447cb54cb" = true
+
+# Translate RNA strand into correct protein list
+"d0f295df-fb70-425c-946c-ec2ec185388e" = true
+
+# Translation stops if STOP codon at beginning of sequence
+"e30e8505-97ec-4e5f-a73e-5726a1faa1f4" = true
+
+# Translation stops if STOP codon at end of two-codon sequence
+"5358a20b-6f4c-4893-bce4-f929001710f3" = true
+
+# Translation stops if STOP codon at end of three-codon sequence
+"ba16703a-1a55-482f-bb07-b21eef5093a3" = true
+
+# Translation stops if STOP codon in middle of three-codon sequence
+"4089bb5a-d5b4-4e71-b79e-b8d1f14a2911" = true
+
+# Translation stops if STOP codon in middle of six-codon sequence
+"2c2a2a60-401f-4a80-b977-e0715b23b93d" = true
diff --git a/exercises/proverb/.meta/tests.toml b/exercises/proverb/.meta/tests.toml
new file mode 100644
index 00000000..efbcc4d3
--- /dev/null
+++ b/exercises/proverb/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# zero pieces
+"e974b73e-7851-484f-8d6d-92e07fe742fc" = true
+
+# one piece
+"2fcd5f5e-8b82-4e74-b51d-df28a5e0faa4" = true
+
+# two pieces
+"d9d0a8a1-d933-46e2-aa94-eecf679f4b0e" = true
+
+# three pieces
+"c95ef757-5e94-4f0d-a6cb-d2083f5e5a83" = true
+
+# full proverb
+"433fb91c-35a2-4d41-aeab-4de1e82b2126" = true
+
+# four pieces modernized
+"c1eefa5a-e8d9-41c7-91d4-99fab6d6b9f7" = true
diff --git a/exercises/pythagorean-triplet/.meta/tests.toml b/exercises/pythagorean-triplet/.meta/tests.toml
new file mode 100644
index 00000000..544d6a14
--- /dev/null
+++ b/exercises/pythagorean-triplet/.meta/tests.toml
@@ -0,0 +1,22 @@
+[canonical-tests]
+
+# triplets whose sum is 12
+"a19de65d-35b8-4480-b1af-371d9541e706" = true
+
+# triplets whose sum is 108
+"48b21332-0a3d-43b2-9a52-90b2a6e5c9f5" = true
+
+# triplets whose sum is 1000
+"dffc1266-418e-4daa-81af-54c3e95c3bb5" = true
+
+# no matching triplets for 1001
+"5f86a2d4-6383-4cce-93a5-e4489e79b186" = true
+
+# returns all matching triplets
+"bf17ba80-1596-409a-bb13-343bdb3b2904" = true
+
+# several matching triplets
+"9d8fb5d5-6c6f-42df-9f95-d3165963ac57" = true
+
+# triplets for large number
+"f5be5734-8aa0-4bd1-99a2-02adcc4402b4" = true
diff --git a/exercises/queen-attack/.meta/tests.toml b/exercises/queen-attack/.meta/tests.toml
new file mode 100644
index 00000000..7b7e90fd
--- /dev/null
+++ b/exercises/queen-attack/.meta/tests.toml
@@ -0,0 +1,37 @@
+[canonical-tests]
+
+# queen with a valid position
+"3ac4f735-d36c-44c4-a3e2-316f79704203" = true
+
+# queen must have positive row
+"4e812d5d-b974-4e38-9a6b-8e0492bfa7be" = true
+
+# queen must have row on board
+"f07b7536-b66b-4f08-beb9-4d70d891d5c8" = true
+
+# queen must have positive column
+"15a10794-36d9-4907-ae6b-e5a0d4c54ebe" = true
+
+# queen must have column on board
+"6907762d-0e8a-4c38-87fb-12f2f65f0ce4" = true
+
+# can not attack
+"33ae4113-d237-42ee-bac1-e1e699c0c007" = true
+
+# can attack on same row
+"eaa65540-ea7c-4152-8c21-003c7a68c914" = true
+
+# can attack on same column
+"bae6f609-2c0e-4154-af71-af82b7c31cea" = true
+
+# can attack on first diagonal
+"0e1b4139-b90d-4562-bd58-dfa04f1746c7" = true
+
+# can attack on second diagonal
+"ff9b7ed4-e4b6-401b-8d16-bc894d6d3dcd" = true
+
+# can attack on third diagonal
+"0a71e605-6e28-4cc2-aa47-d20a2e71037a" = true
+
+# can attack on fourth diagonal
+"0790b588-ae73-4f1f-a968-dd0b34f45f86" = true
diff --git a/exercises/rail-fence-cipher/.meta/tests.toml b/exercises/rail-fence-cipher/.meta/tests.toml
new file mode 100644
index 00000000..6e1a9ab1
--- /dev/null
+++ b/exercises/rail-fence-cipher/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# encode with two rails
+"46dc5c50-5538-401d-93a5-41102680d068" = true
+
+# encode with three rails
+"25691697-fbd8-4278-8c38-b84068b7bc29" = true
+
+# encode with ending in the middle
+"384f0fea-1442-4f1a-a7c4-5cbc2044002c" = true
+
+# decode with three rails
+"cd525b17-ec34-45ef-8f0e-4f27c24a7127" = true
+
+# decode with five rails
+"dd7b4a98-1a52-4e5c-9499-cbb117833507" = true
+
+# decode with six rails
+"93e1ecf4-fac9-45d9-9cd2-591f47d3b8d3" = true
diff --git a/exercises/raindrops/.meta/tests.toml b/exercises/raindrops/.meta/tests.toml
new file mode 100644
index 00000000..ececbef9
--- /dev/null
+++ b/exercises/raindrops/.meta/tests.toml
@@ -0,0 +1,55 @@
+[canonical-tests]
+
+# the sound for 1 is 1
+"1575d549-e502-46d4-a8e1-6b7bec6123d8" = true
+
+# the sound for 3 is Pling
+"1f51a9f9-4895-4539-b182-d7b0a5ab2913" = true
+
+# the sound for 5 is Plang
+"2d9bfae5-2b21-4bcd-9629-c8c0e388f3e0" = true
+
+# the sound for 7 is Plong
+"d7e60daa-32ef-4c23-b688-2abff46c4806" = true
+
+# the sound for 6 is Pling as it has a factor 3
+"6bb4947b-a724-430c-923f-f0dc3d62e56a" = true
+
+# 2 to the power 3 does not make a raindrop sound as 3 is the exponent not the base
+"ce51e0e8-d9d4-446d-9949-96eac4458c2d" = true
+
+# the sound for 9 is Pling as it has a factor 3
+"0dd66175-e3e2-47fc-8750-d01739856671" = true
+
+# the sound for 10 is Plang as it has a factor 5
+"022c44d3-2182-4471-95d7-c575af225c96" = true
+
+# the sound for 14 is Plong as it has a factor of 7
+"37ab74db-fed3-40ff-b7b9-04acdfea8edf" = true
+
+# the sound for 15 is PlingPlang as it has factors 3 and 5
+"31f92999-6afb-40ee-9aa4-6d15e3334d0f" = true
+
+# the sound for 21 is PlingPlong as it has factors 3 and 7
+"ff9bb95d-6361-4602-be2c-653fe5239b54" = true
+
+# the sound for 25 is Plang as it has a factor 5
+"d2e75317-b72e-40ab-8a64-6734a21dece1" = true
+
+# the sound for 27 is Pling as it has a factor 3
+"a09c4c58-c662-4e32-97fe-f1501ef7125c" = true
+
+# the sound for 35 is PlangPlong as it has factors 5 and 7
+"bdf061de-8564-4899-a843-14b48b722789" = true
+
+# the sound for 49 is Plong as it has a factor 7
+"c4680bee-69ba-439d-99b5-70c5fd1a7a83" = true
+
+# the sound for 52 is 52
+"17f2bc9a-b65a-4d23-8ccd-266e8c271444" = true
+
+# the sound for 105 is PlingPlangPlong as it has factors 3, 5 and 7
+"e46677ed-ff1a-419f-a740-5c713d2830e4" = true
+
+# the sound for 3125 is Plang as it has a factor 5
+"13c6837a-0fcd-4b86-a0eb-20572f7deb0b" = true
diff --git a/exercises/rational-numbers/.meta/tests.toml b/exercises/rational-numbers/.meta/tests.toml
new file mode 100644
index 00000000..f935e0e2
--- /dev/null
+++ b/exercises/rational-numbers/.meta/tests.toml
@@ -0,0 +1,115 @@
+[canonical-tests]
+
+# Add two positive rational numbers
+"0ba4d988-044c-4ed5-9215-4d0bb8d0ae9f" = true
+
+# Add a positive rational number and a negative rational number
+"88ebc342-a2ac-4812-a656-7b664f718b6a" = true
+
+# Add two negative rational numbers
+"92ed09c2-991e-4082-a602-13557080205c" = true
+
+# Add a rational number to its additive inverse
+"6e58999e-3350-45fb-a104-aac7f4a9dd11" = true
+
+# Subtract two positive rational numbers
+"47bba350-9db1-4ab9-b412-4a7e1f72a66e" = true
+
+# Subtract a positive rational number and a negative rational number
+"93926e2a-3e82-4aee-98a7-fc33fb328e87" = true
+
+# Subtract two negative rational numbers
+"a965ba45-9b26-442b-bdc7-7728e4b8d4cc" = true
+
+# Subtract a rational number from itself
+"0df0e003-f68e-4209-8c6e-6a4e76af5058" = true
+
+# Multiply two positive rational numbers
+"34fde77a-75f4-4204-8050-8d3a937958d3" = true
+
+# Multiply a negative rational number by a positive rational number
+"6d015cf0-0ea3-41f1-93de-0b8e38e88bae" = true
+
+# Multiply two negative rational numbers
+"d1bf1b55-954e-41b1-8c92-9fc6beeb76fa" = true
+
+# Multiply a rational number by its reciprocal
+"a9b8f529-9ec7-4c79-a517-19365d779040" = true
+
+# Multiply a rational number by 1
+"d89d6429-22fa-4368-ab04-9e01a44d3b48" = true
+
+# Multiply a rational number by 0
+"0d95c8b9-1482-4ed7-bac9-b8694fa90145" = true
+
+# Divide two positive rational numbers
+"1de088f4-64be-4e6e-93fd-5997ae7c9798" = true
+
+# Divide a positive rational number by a negative rational number
+"7d7983db-652a-4e66-981a-e921fb38d9a9" = true
+
+# Divide two negative rational numbers
+"1b434d1b-5b38-4cee-aaf5-b9495c399e34" = true
+
+# Divide a rational number by 1
+"d81c2ebf-3612-45a6-b4e0-f0d47812bd59" = true
+
+# Absolute value of a positive rational number
+"5fee0d8e-5955-4324-acbe-54cdca94ddaa" = true
+
+# Absolute value of a positive rational number with negative numerator and denominator
+"3cb570b6-c36a-4963-a380-c0834321bcaa" = true
+
+# Absolute value of a negative rational number
+"6a05f9a0-1f6b-470b-8ff7-41af81773f25" = true
+
+# Absolute value of a negative rational number with negative denominator
+"5d0f2336-3694-464f-8df9-f5852fda99dd" = true
+
+# Absolute value of zero
+"f8e1ed4b-9dca-47fb-a01e-5311457b3118" = true
+
+# Raise a positive rational number to a positive integer power
+"ea2ad2af-3dab-41e7-bb9f-bd6819668a84" = true
+
+# Raise a negative rational number to a positive integer power
+"8168edd2-0af3-45b1-b03f-72c01332e10a" = true
+
+# Raise zero to an integer power
+"e2f25b1d-e4de-4102-abc3-c2bb7c4591e4" = true
+
+# Raise one to an integer power
+"431cac50-ab8b-4d58-8e73-319d5404b762" = true
+
+# Raise a positive rational number to the power of zero
+"7d164739-d68a-4a9c-b99f-dd77ce5d55e6" = true
+
+# Raise a negative rational number to the power of zero
+"eb6bd5f5-f880-4bcd-8103-e736cb6e41d1" = true
+
+# Raise a real number to a positive rational number
+"30b467dd-c158-46f5-9ffb-c106de2fd6fa" = true
+
+# Raise a real number to a negative rational number
+"6e026bcc-be40-4b7b-ae22-eeaafc5a1789" = true
+
+# Raise a real number to a zero rational number
+"9f866da7-e893-407f-8cd2-ee85d496eec5" = true
+
+# Reduce a positive rational number to lowest terms
+"0a63fbde-b59c-4c26-8237-1e0c73354d0a" = true
+
+# Reduce a negative rational number to lowest terms
+"f87c2a4e-d29c-496e-a193-318c503e4402" = true
+
+# Reduce a rational number with a negative denominator to lowest terms
+"3b92ffc0-5b70-4a43-8885-8acee79cdaaf" = true
+
+# Reduce zero to lowest terms
+"c9dbd2e6-5ac0-4a41-84c1-48b645b4f663" = true
+
+# Reduce an integer to lowest terms
+"297b45ad-2054-4874-84d4-0358dc1b8887" = true
+
+# Reduce one to lowest terms
+"a73a17fe-fe8c-4a1c-a63b-e7579e333d9e" = true
diff --git a/exercises/react/.meta/tests.toml b/exercises/react/.meta/tests.toml
new file mode 100644
index 00000000..071ae848
--- /dev/null
+++ b/exercises/react/.meta/tests.toml
@@ -0,0 +1,43 @@
+[canonical-tests]
+
+# input cells have a value
+"c51ee736-d001-4f30-88d1-0c8e8b43cd07" = true
+
+# an input cell's value can be set
+"dedf0fe0-da0c-4d5d-a582-ffaf5f4d0851" = true
+
+# compute cells calculate initial value
+"5854b975-f545-4f93-8968-cc324cde746e" = true
+
+# compute cells take inputs in the right order
+"25795a3d-b86c-4e91-abe7-1c340e71560c" = true
+
+# compute cells update value when dependencies are changed
+"c62689bf-7be5-41bb-b9f8-65178ef3e8ba" = true
+
+# compute cells can depend on other compute cells
+"5ff36b09-0a88-48d4-b7f8-69dcf3feea40" = true
+
+# compute cells fire callbacks
+"abe33eaf-68ad-42a5-b728-05519ca88d2d" = true
+
+# callback cells only fire on change
+"9e5cb3a4-78e5-4290-80f8-a78612c52db2" = true
+
+# callbacks do not report already reported values
+"ada17cb6-7332-448a-b934-e3d7495c13d3" = true
+
+# callbacks can fire from multiple cells
+"ac271900-ea5c-461c-9add-eeebcb8c03e5" = true
+
+# callbacks can be added and removed
+"95a82dcc-8280-4de3-a4cd-4f19a84e3d6f" = true
+
+# removing a callback multiple times doesn't interfere with other callbacks
+"f2a7b445-f783-4e0e-8393-469ab4915f2a" = true
+
+# callbacks should only be called once even if multiple dependencies change
+"daf6feca-09e0-4ce5-801d-770ddfe1c268" = true
+
+# callbacks should not be called if dependencies change but output value doesn't change
+"9a5b159f-b7aa-4729-807e-f1c38a46d377" = true
diff --git a/exercises/rectangles/.meta/tests.toml b/exercises/rectangles/.meta/tests.toml
new file mode 100644
index 00000000..e76bb9c7
--- /dev/null
+++ b/exercises/rectangles/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# no rows
+"485b7bab-4150-40aa-a8db-73013427d08c" = true
+
+# no columns
+"076929ed-27e8-45dc-b14b-08279944dc49" = true
+
+# no rectangles
+"0a8abbd1-a0a4-4180-aa4e-65c1b1a073fa" = true
+
+# one rectangle
+"a4ba42e9-4e7f-4973-b7c7-4ce0760ac6cd" = true
+
+# two rectangles without shared parts
+"ced06550-83da-4d23-98b7-d24152e0db93" = true
+
+# five rectangles with shared parts
+"5942d69a-a07c-41c8-8b93-2d13877c706a" = true
+
+# rectangle of height 1 is counted
+"82d70be4-ab37-4bf2-a433-e33778d3bbf1" = true
+
+# rectangle of width 1 is counted
+"57f1bc0e-2782-401e-ab12-7c01d8bfc2e0" = true
+
+# 1x1 square is counted
+"ef0bb65c-bd80-4561-9535-efc4067054f9" = true
+
+# only complete rectangles are counted
+"e1e1d444-e926-4d30-9bf3-7d8ec9a9e330" = true
+
+# rectangles can be of different sizes
+"ca021a84-1281-4a56-9b9b-af14113933a4" = true
+
+# corner is required for a rectangle to be complete
+"51f689a7-ef3f-41ae-aa2f-5ea09ad897ff" = true
+
+# large input with many rectangles
+"d78fe379-8c1b-4d3c-bdf7-29bfb6f6dc66" = true
diff --git a/exercises/resistor-color-duo/.meta/tests.toml b/exercises/resistor-color-duo/.meta/tests.toml
new file mode 100644
index 00000000..5e78d911
--- /dev/null
+++ b/exercises/resistor-color-duo/.meta/tests.toml
@@ -0,0 +1,16 @@
+[canonical-tests]
+
+# Brown and black
+"ce11995a-5b93-4950-a5e9-93423693b2fc" = true
+
+# Blue and grey
+"7bf82f7a-af23-48ba-a97d-38d59406a920" = true
+
+# Yellow and violet
+"f1886361-fdfd-4693-acf8-46726fe24e0c" = true
+
+# Orange and orange
+"77a8293d-2a83-4016-b1af-991acc12b9fe" = true
+
+# Ignore additional colors
+"0c4fb44f-db7c-4d03-afa8-054350f156a8" = true
diff --git a/exercises/resistor-color/.meta/tests.toml b/exercises/resistor-color/.meta/tests.toml
new file mode 100644
index 00000000..0eb46566
--- /dev/null
+++ b/exercises/resistor-color/.meta/tests.toml
@@ -0,0 +1,13 @@
+[canonical-tests]
+
+# Black
+"49eb31c5-10a8-4180-9f7f-fea632ab87ef" = true
+
+# White
+"0a4df94b-92da-4579-a907-65040ce0b3fc" = true
+
+# Orange
+"5f81608d-f36f-4190-8084-f45116b6f380" = true
+
+# Colors
+"581d68fa-f968-4be2-9f9d-880f2fb73cf7" = true
diff --git a/exercises/rest-api/.meta/tests.toml b/exercises/rest-api/.meta/tests.toml
new file mode 100644
index 00000000..9741b3b3
--- /dev/null
+++ b/exercises/rest-api/.meta/tests.toml
@@ -0,0 +1,28 @@
+[canonical-tests]
+
+# no users
+"5be01ffb-a814-47a8-a19f-490a5622ba07" = true
+
+# add user
+"382b70cc-9f6c-486d-9bee-fda2df81c803" = true
+
+# get single user
+"d624e5e5-1abb-4f18-95b3-45d55c818dc3" = true
+
+# both users have 0 balance
+"7a81b82c-7276-433e-8fce-29ce983a7c56" = true
+
+# borrower has negative balance
+"1c61f957-cf8c-48ba-9e77-b221ab068803" = true
+
+# lender has negative balance
+"8a8567b3-c097-468a-9541-6bb17d5afc85" = true
+
+# lender owes borrower
+"29fb7c12-7099-4a85-a7c4-9c290d2dc01a" = true
+
+# lender owes borrower less than new loan
+"ce969e70-163c-4135-a4a6-2c3a5da286f5" = true
+
+# lender owes borrower same as new loan
+"7f4aafd9-ae9b-4e15-a406-87a87bdf47a4" = true
diff --git a/exercises/reverse-string/.meta/tests.toml b/exercises/reverse-string/.meta/tests.toml
new file mode 100644
index 00000000..b57518ca
--- /dev/null
+++ b/exercises/reverse-string/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# an empty string
+"c3b7d806-dced-49ee-8543-933fd1719b1c" = true
+
+# a word
+"01ebf55b-bebb-414e-9dec-06f7bb0bee3c" = true
+
+# a capitalized word
+"0f7c07e4-efd1-4aaa-a07a-90b49ce0b746" = true
+
+# a sentence with punctuation
+"71854b9c-f200-4469-9f5c-1e8e5eff5614" = true
+
+# a palindrome
+"1f8ed2f3-56f3-459b-8f3e-6d8d654a1f6c" = true
+
+# an even-sized word
+"b9e7dec1-c6df-40bd-9fa3-cd7ded010c4c" = true
diff --git a/exercises/rna-transcription/.meta/tests.toml b/exercises/rna-transcription/.meta/tests.toml
new file mode 100644
index 00000000..d97f860a
--- /dev/null
+++ b/exercises/rna-transcription/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# Empty RNA sequence
+"b4631f82-c98c-4a2f-90b3-c5c2b6c6f661" = true
+
+# RNA complement of cytosine is guanine
+"a9558a3c-318c-4240-9256-5d5ed47005a6" = true
+
+# RNA complement of guanine is cytosine
+"6eedbb5c-12cb-4c8b-9f51-f8320b4dc2e7" = true
+
+# RNA complement of thymine is adenine
+"870bd3ec-8487-471d-8d9a-a25046488d3e" = true
+
+# RNA complement of adenine is uracil
+"aade8964-02e1-4073-872f-42d3ffd74c5f" = true
+
+# RNA complement
+"79ed2757-f018-4f47-a1d7-34a559392dbf" = true
diff --git a/exercises/robot-simulator/.meta/tests.toml b/exercises/robot-simulator/.meta/tests.toml
new file mode 100644
index 00000000..7244c745
--- /dev/null
+++ b/exercises/robot-simulator/.meta/tests.toml
@@ -0,0 +1,55 @@
+[canonical-tests]
+
+# at origin facing north
+"c557c16d-26c1-4e06-827c-f6602cd0785c" = true
+
+# at negative position facing south
+"bf0dffce-f11c-4cdb-8a5e-2c89d8a5a67d" = true
+
+# changes north to east
+"8cbd0086-6392-4680-b9b9-73cf491e67e5" = true
+
+# changes east to south
+"8abc87fc-eab2-4276-93b7-9c009e866ba1" = true
+
+# changes south to west
+"3cfe1b85-bbf2-4bae-b54d-d73e7e93617a" = true
+
+# changes west to north
+"5ea9fb99-3f2c-47bd-86f7-46b7d8c3c716" = true
+
+# changes north to west
+"fa0c40f5-6ba3-443d-a4b3-58cbd6cb8d63" = true
+
+# changes west to south
+"da33d734-831f-445c-9907-d66d7d2a92e2" = true
+
+# changes south to east
+"bd1ca4b9-4548-45f4-b32e-900fc7c19389" = true
+
+# changes east to north
+"2de27b67-a25c-4b59-9883-bc03b1b55bba" = true
+
+# facing north increments Y
+"f0dc2388-cddc-4f83-9bed-bcf46b8fc7b8" = true
+
+# facing south decrements Y
+"2786cf80-5bbf-44b0-9503-a89a9c5789da" = true
+
+# facing east increments X
+"84bf3c8c-241f-434d-883d-69817dbd6a48" = true
+
+# facing west decrements X
+"bb69c4a7-3bbf-4f64-b415-666fa72d7b04" = true
+
+# moving east and north from README
+"e34ac672-4ed4-4be3-a0b8-d9af259cbaa1" = true
+
+# moving west and north
+"f30e4955-4b47-4aa3-8b39-ae98cfbd515b" = true
+
+# moving west and south
+"3e466bf6-20ab-4d79-8b51-264165182fca" = true
+
+# moving east and north
+"41f0bb96-c617-4e6b-acff-a4b279d44514" = true
diff --git a/exercises/roman-numerals/.meta/tests.toml b/exercises/roman-numerals/.meta/tests.toml
new file mode 100644
index 00000000..42767c90
--- /dev/null
+++ b/exercises/roman-numerals/.meta/tests.toml
@@ -0,0 +1,58 @@
+[canonical-tests]
+
+# 1 is a single I
+"19828a3a-fbf7-4661-8ddd-cbaeee0e2178" = true
+
+# 2 is two I's
+"f088f064-2d35-4476-9a41-f576da3f7b03" = true
+
+# 3 is three I's
+"b374a79c-3bea-43e6-8db8-1286f79c7106" = true
+
+# 4, being 5 - 1, is IV
+"05a0a1d4-a140-4db1-82e8-fcc21fdb49bb" = true
+
+# 5 is a single V
+"57c0f9ad-5024-46ab-975d-de18c430b290" = true
+
+# 6, being 5 + 1, is VI
+"20a2b47f-e57f-4797-a541-0b3825d7f249" = true
+
+# 9, being 10 - 1, is IX
+"ff3fb08c-4917-4aab-9f4e-d663491d083d" = true
+
+# 20 is two X's
+"2bda64ca-7d28-4c56-b08d-16ce65716cf6" = true
+
+# 48 is not 50 - 2 but rather 40 + 8
+"a1f812ef-84da-4e02-b4f0-89c907d0962c" = true
+
+# 49 is not 40 + 5 + 4 but rather 50 - 10 + 10 - 1
+"607ead62-23d6-4c11-a396-ef821e2e5f75" = true
+
+# 50 is a single L
+"d5b283d4-455d-4e68-aacf-add6c4b51915" = true
+
+# 90, being 100 - 10, is XC
+"46b46e5b-24da-4180-bfe2-2ef30b39d0d0" = true
+
+# 100 is a single C
+"30494be1-9afb-4f84-9d71-db9df18b55e3" = true
+
+# 60, being 50 + 10, is LX
+"267f0207-3c55-459a-b81d-67cec7a46ed9" = true
+
+# 400, being 500 - 100, is CD
+"cdb06885-4485-4d71-8bfb-c9d0f496b404" = true
+
+# 500 is a single D
+"6b71841d-13b2-46b4-ba97-dec28133ea80" = true
+
+# 900, being 1000 - 100, is CM
+"432de891-7fd6-4748-a7f6-156082eeca2f" = true
+
+# 1000 is a single M
+"e6de6d24-f668-41c0-88d7-889c0254d173" = true
+
+# 3000 is three M's
+"bb550038-d4eb-4be2-a9ce-f21961ac3bc6" = true
diff --git a/exercises/rotational-cipher/.meta/tests.toml b/exercises/rotational-cipher/.meta/tests.toml
new file mode 100644
index 00000000..446abff5
--- /dev/null
+++ b/exercises/rotational-cipher/.meta/tests.toml
@@ -0,0 +1,31 @@
+[canonical-tests]
+
+# rotate a by 0, same output as input
+"74e58a38-e484-43f1-9466-877a7515e10f" = true
+
+# rotate a by 1
+"7ee352c6-e6b0-4930-b903-d09943ecb8f5" = true
+
+# rotate a by 26, same output as input
+"edf0a733-4231-4594-a5ee-46a4009ad764" = true
+
+# rotate m by 13
+"e3e82cb9-2a5b-403f-9931-e43213879300" = true
+
+# rotate n by 13 with wrap around alphabet
+"19f9eb78-e2ad-4da4-8fe3-9291d47c1709" = true
+
+# rotate capital letters
+"a116aef4-225b-4da9-884f-e8023ca6408a" = true
+
+# rotate spaces
+"71b541bb-819c-4dc6-a9c3-132ef9bb737b" = true
+
+# rotate numbers
+"ef32601d-e9ef-4b29-b2b5-8971392282e6" = true
+
+# rotate punctuation
+"32dd74f6-db2b-41a6-b02c-82eb4f93e549" = true
+
+# rotate all letters
+"9fb93fe6-42b0-46e6-9ec1-0bf0a062d8c9" = true
diff --git a/exercises/run-length-encoding/.meta/tests.toml b/exercises/run-length-encoding/.meta/tests.toml
new file mode 100644
index 00000000..f06c8d6d
--- /dev/null
+++ b/exercises/run-length-encoding/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# empty string
+"ad53b61b-6ffc-422f-81a6-61f7df92a231" = true
+
+# single characters only are encoded without count
+"52012823-b7e6-4277-893c-5b96d42f82de" = true
+
+# string with no single characters
+"b7868492-7e3a-415f-8da3-d88f51f80409" = true
+
+# single characters mixed with repeated characters
+"859b822b-6e9f-44d6-9c46-6091ee6ae358" = true
+
+# multiple whitespace mixed in string
+"1b34de62-e152-47be-bc88-469746df63b3" = true
+
+# lowercase characters
+"abf176e2-3fbd-40ad-bb2f-2dd6d4df721a" = true
+
+# empty string
+"7ec5c390-f03c-4acf-ac29-5f65861cdeb5" = true
+
+# single characters only
+"ad23f455-1ac2-4b0e-87d0-b85b10696098" = true
+
+# string with no single characters
+"21e37583-5a20-4a0e-826c-3dee2c375f54" = true
+
+# single characters with repeated characters
+"1389ad09-c3a8-4813-9324-99363fba429c" = true
+
+# multiple whitespace mixed in string
+"3f8e3c51-6aca-4670-b86c-a213bf4706b0" = true
+
+# lower case string
+"29f721de-9aad-435f-ba37-7662df4fb551" = true
+
+# encode followed by decode gives original string
+"2a762efd-8695-4e04-b0d6-9736899fbc16" = true
diff --git a/exercises/saddle-points/.meta/tests.toml b/exercises/saddle-points/.meta/tests.toml
new file mode 100644
index 00000000..edff0d8f
--- /dev/null
+++ b/exercises/saddle-points/.meta/tests.toml
@@ -0,0 +1,28 @@
+[canonical-tests]
+
+# Can identify single saddle point
+"3e374e63-a2e0-4530-a39a-d53c560382bd" = true
+
+# Can identify that empty matrix has no saddle points
+"6b501e2b-6c1f-491f-b1bb-7f278f760534" = true
+
+# Can identify lack of saddle points when there are none
+"8c27cc64-e573-4fcb-a099-f0ae863fb02f" = true
+
+# Can identify multiple saddle points in a column
+"6d1399bd-e105-40fd-a2c9-c6609507d7a3" = true
+
+# Can identify multiple saddle points in a row
+"3e81dce9-53b3-44e6-bf26-e328885fd5d1" = true
+
+# Can identify saddle point in bottom right corner
+"88868621-b6f4-4837-bb8b-3fad8b25d46b" = true
+
+# Can identify saddle points in a non square matrix
+"5b9499ca-fcea-4195-830a-9c4584a0ee79" = true
+
+# Can identify that saddle points in a single column matrix are those with the minimum value
+"ee99ccd2-a1f1-4283-ad39-f8c70f0cf594" = true
+
+# Can identify that saddle points in a single row matrix are those with the maximum value
+"63abf709-a84b-407f-a1b3-456638689713" = true
diff --git a/exercises/satellite/.meta/tests.toml b/exercises/satellite/.meta/tests.toml
new file mode 100644
index 00000000..2f6e6706
--- /dev/null
+++ b/exercises/satellite/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# Empty tree
+"8df3fa26-811a-4165-9286-ff9ac0850d19" = true
+
+# Tree with one item
+"f945ccfc-05e3-47d7-825b-0270559d43ad" = true
+
+# Tree with many items
+"a0121d5f-37b0-48dd-9c64-cba4c4464135" = true
+
+# Reject traversals of different length
+"6074041f-4891-4d81-a128-401050c2a3b0" = true
+
+# Reject inconsistent traversals of same length
+"27916ce4-45f3-4d8b-8528-496fedc157ca" = true
+
+# Reject traversals with repeated items
+"d86a3d72-76a9-43b5-9d3a-e64cb1216035" = true
diff --git a/exercises/say/.meta/tests.toml b/exercises/say/.meta/tests.toml
new file mode 100644
index 00000000..99c8473f
--- /dev/null
+++ b/exercises/say/.meta/tests.toml
@@ -0,0 +1,46 @@
+[canonical-tests]
+
+# zero
+"5d22a120-ba0c-428c-bd25-8682235d83e8" = true
+
+# one
+"9b5eed77-dbf6-439d-b920-3f7eb58928f6" = true
+
+# fourteen
+"7c499be1-612e-4096-a5e1-43b2f719406d" = true
+
+# twenty
+"f541dd8e-f070-4329-92b4-b7ce2fcf06b4" = true
+
+# twenty-two
+"d78601eb-4a84-4bfa-bf0e-665aeb8abe94" = true
+
+# one hundred
+"e417d452-129e-4056-bd5b-6eb1df334dce" = true
+
+# one hundred twenty-three
+"d6924f30-80ba-4597-acf6-ea3f16269da8" = true
+
+# one thousand
+"3d83da89-a372-46d3-b10d-de0c792432b3" = true
+
+# one thousand two hundred thirty-four
+"865af898-1d5b-495f-8ff0-2f06d3c73709" = true
+
+# one million
+"b6a3f442-266e-47a3-835d-7f8a35f6cf7f" = true
+
+# one million two thousand three hundred forty-five
+"2cea9303-e77e-4212-b8ff-c39f1978fc70" = true
+
+# one billion
+"3e240eeb-f564-4b80-9421-db123f66a38f" = true
+
+# a big number
+"9a43fed1-c875-4710-8286-5065d73b8a9e" = true
+
+# numbers below zero are out of range
+"49a6a17b-084e-423e-994d-a87c0ecc05ef" = true
+
+# numbers above 999,999,999,999 are out of range
+"4d6492eb-5853-4d16-9d34-b0f61b261fd9" = true
diff --git a/exercises/scale-generator/.meta/tests.toml b/exercises/scale-generator/.meta/tests.toml
new file mode 100644
index 00000000..d44f1e4e
--- /dev/null
+++ b/exercises/scale-generator/.meta/tests.toml
@@ -0,0 +1,52 @@
+[canonical-tests]
+
+# Chromatic scale with sharps
+"10ea7b14-8a49-40be-ac55-7c62b55f9b47" = true
+
+# Chromatic scale with flats
+"af8381de-9a72-4efd-823a-48374dbfe76f" = true
+
+# Simple major scale
+"6f5b1410-1dd7-4c6c-b410-6b7e986f6f1e" = true
+
+# Major scale with sharps
+"13a92f89-a83e-40b5-b9d4-01136931ba02" = true
+
+# Major scale with flats
+"aa3320f6-a761-49a1-bcf6-978e0c81080a" = true
+
+# Minor scale with sharps
+"63daeb2f-c3f9-4c45-92be-5bf97f61ff94" = true
+
+# Minor scale with flats
+"616594d0-9c48-4301-949e-af1d4fad16fd" = true
+
+# Dorian mode
+"390bd12c-5ac7-4ec7-bdde-4e58d5c78b0a" = true
+
+# Mixolydian mode
+"846d0862-0f3e-4f3b-8a2d-9cc74f017848" = true
+
+# Lydian mode
+"7d49a8bb-b5f7-46ad-a207-83bd5032291a" = true
+
+# Phrygian mode
+"a4e4dac5-1891-4160-a19f-bb06d653d4d0" = true
+
+# Locrian mode
+"ef3650af-90f8-4ad9-9ef6-fdbeae07dcaa" = true
+
+# Harmonic minor
+"70517400-12b7-4530-b861-fa940ae69ee8" = true
+
+# Octatonic
+"37114c0b-c54d-45da-9f4b-3848201470b0" = true
+
+# Hexatonic
+"496466e7-aa45-4bbd-a64d-f41030feed9c" = true
+
+# Pentatonic
+"bee5d9ec-e226-47b6-b62b-847a9241f3cc" = true
+
+# Enigmatic
+"dbee06a6-7535-4ab7-98e8-d8a36c8402d1" = true
diff --git a/exercises/scrabble-score/.meta/tests.toml b/exercises/scrabble-score/.meta/tests.toml
new file mode 100644
index 00000000..5357235b
--- /dev/null
+++ b/exercises/scrabble-score/.meta/tests.toml
@@ -0,0 +1,34 @@
+[canonical-tests]
+
+# lowercase letter
+"f46cda29-1ca5-4ef2-bd45-388a767e3db2" = true
+
+# uppercase letter
+"f7794b49-f13e-45d1-a933-4e48459b2201" = true
+
+# valuable letter
+"eaba9c76-f9fa-49c9-a1b0-d1ba3a5b31fa" = true
+
+# short word
+"f3c8c94e-bb48-4da2-b09f-e832e103151e" = true
+
+# short, valuable word
+"71e3d8fa-900d-4548-930e-68e7067c4615" = true
+
+# medium word
+"d3088ad9-570c-4b51-8764-c75d5a430e99" = true
+
+# medium, valuable word
+"fa20c572-ad86-400a-8511-64512daac352" = true
+
+# long, mixed-case word
+"9336f0ba-9c2b-4fa0-bd1c-2e2d328cf967" = true
+
+# english-like word
+"1e34e2c3-e444-4ea7-b598-3c2b46fd2c10" = true
+
+# empty input
+"4efe3169-b3b6-4334-8bae-ff4ef24a7e4f" = true
+
+# entire alphabet available
+"3b305c1c-f260-4e15-a5b5-cb7d3ea7c3d7" = true
diff --git a/exercises/secret-handshake/.meta/tests.toml b/exercises/secret-handshake/.meta/tests.toml
new file mode 100644
index 00000000..7f445862
--- /dev/null
+++ b/exercises/secret-handshake/.meta/tests.toml
@@ -0,0 +1,34 @@
+[canonical-tests]
+
+# wink for 1
+"b8496fbd-6778-468c-8054-648d03c4bb23" = true
+
+# double blink for 10
+"83ec6c58-81a9-4fd1-bfaf-0160514fc0e3" = true
+
+# close your eyes for 100
+"0e20e466-3519-4134-8082-5639d85fef71" = true
+
+# jump for 1000
+"b339ddbb-88b7-4b7d-9b19-4134030d9ac0" = true
+
+# combine two actions
+"40499fb4-e60c-43d7-8b98-0de3ca44e0eb" = true
+
+# reverse two actions
+"9730cdd5-ef27-494b-afd3-5c91ad6c3d9d" = true
+
+# reversing one action gives the same action
+"0b828205-51ca-45cd-90d5-f2506013f25f" = true
+
+# reversing no actions still gives no actions
+"9949e2ac-6c9c-4330-b685-2089ab28b05f" = true
+
+# all possible actions
+"23fdca98-676b-4848-970d-cfed7be39f81" = true
+
+# reverse all possible actions
+"ae8fe006-d910-4d6f-be00-54b7c3799e79" = true
+
+# do nothing for zero
+"3d36da37-b31f-4cdb-a396-d93a2ee1c4a5" = true
diff --git a/exercises/series/.meta/tests.toml b/exercises/series/.meta/tests.toml
new file mode 100644
index 00000000..3e05ec7e
--- /dev/null
+++ b/exercises/series/.meta/tests.toml
@@ -0,0 +1,31 @@
+[canonical-tests]
+
+# slices of one from one
+"7ae7a46a-d992-4c2a-9c15-a112d125ebad" = true
+
+# slices of one from two
+"3143b71d-f6a5-4221-aeae-619f906244d2" = true
+
+# slices of two
+"dbb68ff5-76c5-4ccd-895a-93dbec6d5805" = true
+
+# slices of two overlap
+"19bbea47-c987-4e11-a7d1-e103442adf86" = true
+
+# slices can include duplicates
+"8e17148d-ba0a-4007-a07f-d7f87015d84c" = true
+
+# slices of a long series
+"bd5b085e-f612-4f81-97a8-6314258278b0" = true
+
+# slice length is too large
+"6d235d85-46cf-4fae-9955-14b6efef27cd" = true
+
+# slice length cannot be zero
+"d34004ad-8765-4c09-8ba1-ada8ce776806" = true
+
+# slice length cannot be negative
+"10ab822d-8410-470a-a85d-23fbeb549e54" = true
+
+# empty series is invalid
+"c7ed0812-0e4b-4bf3-99c4-28cbbfc246a2" = true
diff --git a/exercises/sgf-parsing/.meta/tests.toml b/exercises/sgf-parsing/.meta/tests.toml
new file mode 100644
index 00000000..41098717
--- /dev/null
+++ b/exercises/sgf-parsing/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# empty input
+"2668d5dc-109f-4f71-b9d5-8d06b1d6f1cd" = true
+
+# tree with no nodes
+"84ded10a-94df-4a30-9457-b50ccbdca813" = true
+
+# node without tree
+"0a6311b2-c615-4fa7-800e-1b1cbb68833d" = true
+
+# node without properties
+"8c419ed8-28c4-49f6-8f2d-433e706110ef" = true
+
+# single node tree
+"8209645f-32da-48fe-8e8f-b9b562c26b49" = true
+
+# multiple properties
+"6c995856-b919-4c75-8fd6-c2c3c31b37dc" = true
+
+# properties without delimiter
+"a771f518-ec96-48ca-83c7-f8d39975645f" = true
+
+# all lowercase property
+"6c02a24e-6323-4ed5-9962-187d19e36bc8" = true
+
+# upper and lowercase property
+"8772d2b1-3c57-405a-93ac-0703b671adc1" = true
+
+# two nodes
+"a759b652-240e-42ec-a6d2-3a08d834b9e2" = true
+
+# two child trees
+"cc7c02bc-6097-42c4-ab88-a07cb1533d00" = true
+
+# multiple property values
+"724eeda6-00db-41b1-8aa9-4d5238ca0130" = true
+
+# escaped property
+"11c36323-93fc-495d-bb23-c88ee5844b8c" = true
diff --git a/exercises/sieve/.meta/tests.toml b/exercises/sieve/.meta/tests.toml
new file mode 100644
index 00000000..efe8c0bd
--- /dev/null
+++ b/exercises/sieve/.meta/tests.toml
@@ -0,0 +1,16 @@
+[canonical-tests]
+
+# no primes under two
+"88529125-c4ce-43cc-bb36-1eb4ddd7b44f" = true
+
+# find first prime
+"4afe9474-c705-4477-9923-840e1024cc2b" = true
+
+# find primes up to 10
+"974945d8-8cd9-4f00-9463-7d813c7f17b7" = true
+
+# limit is prime
+"2e2417b7-3f3a-452a-8594-b9af08af6d82" = true
+
+# find primes up to 1000
+"92102a05-4c7c-47de-9ed0-b7d5fcd00f21" = true
diff --git a/exercises/simple-cipher/.meta/tests.toml b/exercises/simple-cipher/.meta/tests.toml
new file mode 100644
index 00000000..5bc43b88
--- /dev/null
+++ b/exercises/simple-cipher/.meta/tests.toml
@@ -0,0 +1,37 @@
+[canonical-tests]
+
+# Can encode
+"b8bdfbe1-bea3-41bb-a999-b41403f2b15d" = true
+
+# Can decode
+"3dff7f36-75db-46b4-ab70-644b3f38b81c" = true
+
+# Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method
+"8143c684-6df6-46ba-bd1f-dea8fcb5d265" = true
+
+# Key is made only of lowercase letters
+"defc0050-e87d-4840-85e4-51a1ab9dd6aa" = true
+
+# Can encode
+"565e5158-5b3b-41dd-b99d-33b9f413c39f" = true
+
+# Can decode
+"d44e4f6a-b8af-4e90-9d08-fd407e31e67b" = true
+
+# Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method
+"70a16473-7339-43df-902d-93408c69e9d1" = true
+
+# Can double shift encode
+"69a1458b-92a6-433a-a02d-7beac3ea91f9" = true
+
+# Can wrap on encode
+"21d207c1-98de-40aa-994f-86197ae230fb" = true
+
+# Can wrap on decode
+"a3d7a4d7-24a9-4de6-bdc4-a6614ced0cb3" = true
+
+# Can encode messages longer than the key
+"e31c9b8c-8eb6-45c9-a4b5-8344a36b9641" = true
+
+# Can decode messages longer than the key
+"93cfaae0-17da-4627-9a04-d6d1e1be52e3" = true
diff --git a/exercises/space-age/.meta/tests.toml b/exercises/space-age/.meta/tests.toml
new file mode 100644
index 00000000..52de49cf
--- /dev/null
+++ b/exercises/space-age/.meta/tests.toml
@@ -0,0 +1,25 @@
+[canonical-tests]
+
+# age on Earth
+"84f609af-5a91-4d68-90a3-9e32d8a5cd34" = true
+
+# age on Mercury
+"ca20c4e9-6054-458c-9312-79679ffab40b" = true
+
+# age on Venus
+"502c6529-fd1b-41d3-8fab-65e03082b024" = true
+
+# age on Mars
+"9ceadf5e-a0d5-4388-9d40-2c459227ceb8" = true
+
+# age on Jupiter
+"42927dc3-fe5e-4f76-a5b5-f737fc19bcde" = true
+
+# age on Saturn
+"8469b332-7837-4ada-b27c-00ee043ebcad" = true
+
+# age on Uranus
+"999354c1-76f8-4bb5-a672-f317b6436743" = true
+
+# age on Neptune
+"80096d30-a0d4-4449-903e-a381178355d8" = true
diff --git a/exercises/spiral-matrix/.meta/tests.toml b/exercises/spiral-matrix/.meta/tests.toml
new file mode 100644
index 00000000..f0868d37
--- /dev/null
+++ b/exercises/spiral-matrix/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# empty spiral
+"8f584201-b446-4bc9-b132-811c8edd9040" = true
+
+# trivial spiral
+"e40ae5f3-e2c9-4639-8116-8a119d632ab2" = true
+
+# spiral of size 2
+"cf05e42d-eb78-4098-a36e-cdaf0991bc48" = true
+
+# spiral of size 3
+"1c475667-c896-4c23-82e2-e033929de939" = true
+
+# spiral of size 4
+"05ccbc48-d891-44f5-9137-f4ce462a759d" = true
+
+# spiral of size 5
+"f4d2165b-1738-4e0c-bed0-c459045ae50d" = true
diff --git a/exercises/sublist/.meta/tests.toml b/exercises/sublist/.meta/tests.toml
new file mode 100644
index 00000000..9a06e444
--- /dev/null
+++ b/exercises/sublist/.meta/tests.toml
@@ -0,0 +1,52 @@
+[canonical-tests]
+
+# empty lists
+"97319c93-ebc5-47ab-a022-02a1980e1d29" = true
+
+# empty list within non empty list
+"de27dbd4-df52-46fe-a336-30be58457382" = true
+
+# non empty list contains empty list
+"5487cfd1-bc7d-429f-ac6f-1177b857d4fb" = true
+
+# list equals itself
+"1f390b47-f6b2-4a93-bc23-858ba5dda9a6" = true
+
+# different lists
+"7ed2bfb2-922b-4363-ae75-f3a05e8274f5" = true
+
+# false start
+"3b8a2568-6144-4f06-b0a1-9d266b365341" = true
+
+# consecutive
+"dc39ed58-6311-4814-be30-05a64bc8d9b1" = true
+
+# sublist at start
+"d1270dab-a1ce-41aa-b29d-b3257241ac26" = true
+
+# sublist in middle
+"81f3d3f7-4f25-4ada-bcdc-897c403de1b6" = true
+
+# sublist at end
+"43bcae1e-a9cf-470e-923e-0946e04d8fdd" = true
+
+# at start of superlist
+"76cf99ed-0ff0-4b00-94af-4dfb43fe5caa" = true
+
+# in middle of superlist
+"b83989ec-8bdf-4655-95aa-9f38f3e357fd" = true
+
+# at end of superlist
+"26f9f7c3-6cf6-4610-984a-662f71f8689b" = true
+
+# first list missing element from second list
+"0a6db763-3588-416a-8f47-76b1cedde31e" = true
+
+# second list missing element from first list
+"83ffe6d8-a445-4a3c-8795-1e51a95e65c3" = true
+
+# order matters to a list
+"0d7ee7c1-0347-45c8-9ef5-b88db152b30b" = true
+
+# same digits but different numbers
+"5f47ce86-944e-40f9-9f31-6368aad70aa6" = true
diff --git a/exercises/sum-of-multiples/.meta/tests.toml b/exercises/sum-of-multiples/.meta/tests.toml
new file mode 100644
index 00000000..49cb1232
--- /dev/null
+++ b/exercises/sum-of-multiples/.meta/tests.toml
@@ -0,0 +1,49 @@
+[canonical-tests]
+
+# no multiples within limit
+"54aaab5a-ce86-4edc-8b40-d3ab2400a279" = true
+
+# one factor has multiples within limit
+"361e4e50-c89b-4f60-95ef-5bc5c595490a" = true
+
+# more than one multiple within limit
+"e644e070-040e-4ae0-9910-93c69fc3f7ce" = true
+
+# more than one factor with multiples within limit
+"607d6eb9-535c-41ce-91b5-3a61da3fa57f" = true
+
+# each multiple is only counted once
+"f47e8209-c0c5-4786-b07b-dc273bf86b9b" = true
+
+# a much larger limit
+"28c4b267-c980-4054-93e9-07723db615ac" = true
+
+# three factors
+"09c4494d-ff2d-4e0f-8421-f5532821ee12" = true
+
+# factors not relatively prime
+"2d0d5faa-f177-4ad6-bde9-ebb865083751" = true
+
+# some pairs of factors relatively prime and some not
+"ece8f2e8-96aa-4166-bbb7-6ce71261e354" = true
+
+# one factor is a multiple of another
+"624fdade-6ffb-400e-8472-456a38c171c0" = true
+
+# much larger factors
+"949ee7eb-db51-479c-b5cb-4a22b40ac057" = true
+
+# all numbers are multiples of 1
+"41093673-acbd-482c-ab80-d00a0cbedecd" = true
+
+# no factors means an empty sum
+"1730453b-baaa-438e-a9c2-d754497b2a76" = true
+
+# the only multiple of 0 is 0
+"214a01e9-f4bf-45bb-80f1-1dce9fbb0310" = true
+
+# the factor 0 does not affect the sum of multiples of other factors
+"c423ae21-a0cb-4ec7-aeb1-32971af5b510" = true
+
+# solutions using include-exclude must extend to cardinality greater than 3
+"17053ba9-112f-4ac0-aadb-0519dd836342" = true
diff --git a/exercises/tournament/.meta/tests.toml b/exercises/tournament/.meta/tests.toml
new file mode 100644
index 00000000..1f91f2ea
--- /dev/null
+++ b/exercises/tournament/.meta/tests.toml
@@ -0,0 +1,34 @@
+[canonical-tests]
+
+# just the header if no input
+"67e9fab1-07c1-49cf-9159-bc8671cc7c9c" = true
+
+# a win is three points, a loss is zero points
+"1b4a8aef-0734-4007-80a2-0626178c88f4" = true
+
+# a win can also be expressed as a loss
+"5f45ac09-4efe-46e7-8ddb-75ad85f86e05" = true
+
+# a different team can win
+"fd297368-efa0-442d-9f37-dd3f9a437239" = true
+
+# a draw is one point each
+"26c016f9-e753-4a93-94e9-842f7b4d70fc" = true
+
+# There can be more than one match
+"731204f6-4f34-4928-97eb-1c307ba83e62" = true
+
+# There can be more than one winner
+"49dc2463-42af-4ea6-95dc-f06cc5776adf" = true
+
+# There can be more than two teams
+"6d930f33-435c-4e6f-9e2d-63fa85ce7dc7" = true
+
+# typical input
+"97022974-0c8a-4a50-8fe7-e36bdd8a5945" = true
+
+# incomplete competition (not all pairs have played)
+"fe562f0d-ac0a-4c62-b9c9-44ee3236392b" = true
+
+# ties broken alphabetically
+"3aa0386f-150b-4f99-90bb-5195e7b7d3b8" = true
diff --git a/exercises/transpose/.meta/tests.toml b/exercises/transpose/.meta/tests.toml
new file mode 100644
index 00000000..715a1504
--- /dev/null
+++ b/exercises/transpose/.meta/tests.toml
@@ -0,0 +1,34 @@
+[canonical-tests]
+
+# empty string
+"404b7262-c050-4df0-a2a2-0cb06cd6a821" = true
+
+# two characters in a row
+"a89ce8a3-c940-4703-a688-3ea39412fbcb" = true
+
+# two characters in a column
+"855bb6ae-4180-457c-abd0-ce489803ce98" = true
+
+# simple
+"5ceda1c0-f940-441c-a244-0ced197769c8" = true
+
+# single line
+"a54675dd-ae7d-4a58-a9c4-0c20e99a7c1f" = true
+
+# first line longer than second line
+"0dc2ec0b-549d-4047-aeeb-8029fec8d5c5" = true
+
+# second line longer than first line
+"984e2ec3-b3d3-4b53-8bd6-96f5ef404102" = true
+
+# mixed line length
+"eccd3784-45f0-4a3f-865a-360cb323d314" = true
+
+# square
+"85b96b3f-d00c-4f80-8ca2-c8a5c9216c2d" = true
+
+# rectangle
+"b9257625-7a53-4748-8863-e08e9d27071d" = true
+
+# triangle
+"b80badc9-057e-4543-bd07-ce1296a1ea2c" = true
diff --git a/exercises/triangle/.meta/tests.toml b/exercises/triangle/.meta/tests.toml
new file mode 100644
index 00000000..182b8475
--- /dev/null
+++ b/exercises/triangle/.meta/tests.toml
@@ -0,0 +1,58 @@
+[canonical-tests]
+
+# all sides are equal
+"8b2c43ac-7257-43f9-b552-7631a91988af" = true
+
+# any side is unequal
+"33eb6f87-0498-4ccf-9573-7f8c3ce92b7b" = true
+
+# no sides are equal
+"c6585b7d-a8c0-4ad8-8a34-e21d36f7ad87" = true
+
+# all zero sides is not a triangle
+"16e8ceb0-eadb-46d1-b892-c50327479251" = true
+
+# sides may be floats
+"3022f537-b8e5-4cc1-8f12-fd775827a00c" = true
+
+# last two sides are equal
+"cbc612dc-d75a-4c1c-87fc-e2d5edd70b71" = true
+
+# first two sides are equal
+"e388ce93-f25e-4daf-b977-4b7ede992217" = true
+
+# first and last sides are equal
+"d2080b79-4523-4c3f-9d42-2da6e81ab30f" = true
+
+# equilateral triangles are also isosceles
+"8d71e185-2bd7-4841-b7e1-71689a5491d8" = true
+
+# no sides are equal
+"840ed5f8-366f-43c5-ac69-8f05e6f10bbb" = true
+
+# first triangle inequality violation
+"2eba0cfb-6c65-4c40-8146-30b608905eae" = true
+
+# second triangle inequality violation
+"278469cb-ac6b-41f0-81d4-66d9b828f8ac" = true
+
+# third triangle inequality violation
+"90efb0c7-72bb-4514-b320-3a3892e278ff" = true
+
+# sides may be floats
+"adb4ee20-532f-43dc-8d31-e9271b7ef2bc" = true
+
+# no sides are equal
+"e8b5f09c-ec2e-47c1-abec-f35095733afb" = true
+
+# all sides are equal
+"2510001f-b44d-4d18-9872-2303e7977dc1" = true
+
+# two sides are equal
+"c6e15a92-90d9-4fb3-90a2-eef64f8d3e1e" = true
+
+# may not violate triangle inequality
+"70ad5154-0033-48b7-af2c-b8d739cd9fdc" = true
+
+# sides may be floats
+"26d9d59d-f8f1-40d3-ad58-ae4d54123d7d" = true
diff --git a/exercises/trinary/.meta/tests.toml b/exercises/trinary/.meta/tests.toml
new file mode 100644
index 00000000..77f66278
--- /dev/null
+++ b/exercises/trinary/.meta/tests.toml
@@ -0,0 +1,34 @@
+[canonical-tests]
+
+# trinary 1 is decimal 1
+"a7a79a9e-5606-454c-9cdb-4f3c0ca46931" = true
+
+# trinary 2 is decimal 2
+"39240078-13e2-4eb8-87c4-aeffa7d64130" = true
+
+# trinary 10 is decimal 3
+"81900d67-7e07-4d41-a71e-86f1cd72ce1f" = true
+
+# trinary 11 is decimal 4
+"7a8d5341-f88a-4c60-9048-4d5e017fa701" = true
+
+# trinary 100 is decimal 9
+"6b3c37f6-d6b3-4575-85c0-19f48dd101af" = true
+
+# trinary 112 is decimal 14
+"a210b2b8-d333-4e19-9e59-87cabdd2a0ba" = true
+
+# trinary 222 is decimal 26
+"5ae03472-b942-42ce-ba00-e84a7dc86dd8" = true
+
+# trinary 1122000120 is decimal 32091
+"d4fabf94-6149-4d1e-b42f-b34dc3ddef8f" = true
+
+# invalid trinary digits returns 0
+"34be152d-38f3-4dcf-b5ab-9e14fe2f7161" = true
+
+# invalid word as input returns 0
+"b57aa24d-3da2-4787-9429-5bc94d3112d6" = true
+
+# invalid numbers with letters as input returns 0
+"673c2057-5d89-483c-87fa-139da6927b90" = true
diff --git a/exercises/twelve-days/.meta/tests.toml b/exercises/twelve-days/.meta/tests.toml
new file mode 100644
index 00000000..ee4722d4
--- /dev/null
+++ b/exercises/twelve-days/.meta/tests.toml
@@ -0,0 +1,46 @@
+[canonical-tests]
+
+# first day a partridge in a pear tree
+"c0b5a5e6-c89d-49b1-a6b2-9f523bff33f7" = true
+
+# second day two turtle doves
+"1c64508a-df3d-420a-b8e1-fe408847854a" = true
+
+# third day three french hens
+"a919e09c-75b2-4e64-bb23-de4a692060a8" = true
+
+# fourth day four calling birds
+"9bed8631-ec60-4894-a3bb-4f0ec9fbe68d" = true
+
+# fifth day five gold rings
+"cf1024f0-73b6-4545-be57-e9cea565289a" = true
+
+# sixth day six geese-a-laying
+"50bd3393-868a-4f24-a618-68df3d02ff04" = true
+
+# seventh day seven swans-a-swimming
+"8f29638c-9bf1-4680-94be-e8b84e4ade83" = true
+
+# eighth day eight maids-a-milking
+"7038d6e1-e377-47ad-8c37-10670a05bc05" = true
+
+# ninth day nine ladies dancing
+"37a800a6-7a56-4352-8d72-0f51eb37cfe8" = true
+
+# tenth day ten lords-a-leaping
+"10b158aa-49ff-4b2d-afc3-13af9133510d" = true
+
+# eleventh day eleven pipers piping
+"08d7d453-f2ba-478d-8df0-d39ea6a4f457" = true
+
+# twelfth day twelve drummers drumming
+"0620fea7-1704-4e48-b557-c05bf43967f0" = true
+
+# recites first three verses of the song
+"da8b9013-b1e8-49df-b6ef-ddec0219e398" = true
+
+# recites three verses from the middle of the song
+"c095af0d-3137-4653-ad32-bfb899eda24c" = true
+
+# recites the whole song
+"20921bc9-cc52-4627-80b3-198cbbfcf9b7" = true
diff --git a/exercises/two-bucket/.meta/tests.toml b/exercises/two-bucket/.meta/tests.toml
new file mode 100644
index 00000000..a323742d
--- /dev/null
+++ b/exercises/two-bucket/.meta/tests.toml
@@ -0,0 +1,19 @@
+[canonical-tests]
+
+# Measure using bucket one of size 3 and bucket two of size 5 - start with bucket one
+"a6f2b4ba-065f-4dca-b6f0-e3eee51cb661" = true
+
+# Measure using bucket one of size 3 and bucket two of size 5 - start with bucket two
+"6c4ea451-9678-4926-b9b3-68364e066d40" = true
+
+# Measure using bucket one of size 7 and bucket two of size 11 - start with bucket one
+"3389f45e-6a56-46d5-9607-75aa930502ff" = true
+
+# Measure using bucket one of size 7 and bucket two of size 11 - start with bucket two
+"fe0ff9a0-3ea5-4bf7-b17d-6d4243961aa1" = true
+
+# Measure one step using bucket one of size 1 and bucket two of size 3 - start with bucket two
+"0ee1f57e-da84-44f7-ac91-38b878691602" = true
+
+# Measure using bucket one of size 2 and bucket two of size 3 - start with bucket one and end with bucket two
+"eb329c63-5540-4735-b30b-97f7f4df0f84" = true
diff --git a/exercises/two-fer/.meta/tests.toml b/exercises/two-fer/.meta/tests.toml
new file mode 100644
index 00000000..6c08723e
--- /dev/null
+++ b/exercises/two-fer/.meta/tests.toml
@@ -0,0 +1,10 @@
+[canonical-tests]
+
+# no name given
+"1cf3e15a-a3d7-4a87-aeb3-ba1b43bc8dce" = true
+
+# a name given
+"b4c6dbb8-b4fb-42c2-bafd-10785abe7709" = true
+
+# another name given
+"3549048d-1a6e-4653-9a79-b0bda163e8d5" = true
diff --git a/exercises/variable-length-quantity/.meta/tests.toml b/exercises/variable-length-quantity/.meta/tests.toml
new file mode 100644
index 00000000..4ee8b6ef
--- /dev/null
+++ b/exercises/variable-length-quantity/.meta/tests.toml
@@ -0,0 +1,79 @@
+[canonical-tests]
+
+# zero
+"35c9db2e-f781-4c52-b73b-8e76427defd0" = true
+
+# arbitrary single byte
+"be44d299-a151-4604-a10e-d4b867f41540" = true
+
+# largest single byte
+"ea399615-d274-4af6-bbef-a1c23c9e1346" = true
+
+# smallest double byte
+"77b07086-bd3f-4882-8476-8dcafee79b1c" = true
+
+# arbitrary double byte
+"63955a49-2690-4e22-a556-0040648d6b2d" = true
+
+# largest double byte
+"29da7031-0067-43d3-83a7-4f14b29ed97a" = true
+
+# smallest triple byte
+"3345d2e3-79a9-4999-869e-d4856e3a8e01" = true
+
+# arbitrary triple byte
+"5df0bc2d-2a57-4300-a653-a75ee4bd0bee" = true
+
+# largest triple byte
+"f51d8539-312d-4db1-945c-250222c6aa22" = true
+
+# smallest quadruple byte
+"da78228b-544f-47b7-8bfe-d16b35bbe570" = true
+
+# arbitrary quadruple byte
+"11ed3469-a933-46f1-996f-2231e05d7bb6" = true
+
+# largest quadruple byte
+"d5f3f3c3-e0f1-4e7f-aad0-18a44f223d1c" = true
+
+# smallest quintuple byte
+"91a18b33-24e7-4bfb-bbca-eca78ff4fc47" = true
+
+# arbitrary quintuple byte
+"5f34ff12-2952-4669-95fe-2d11b693d331" = true
+
+# maximum 32-bit integer input
+"7489694b-88c3-4078-9864-6fe802411009" = true
+
+# two single-byte values
+"f9b91821-cada-4a73-9421-3c81d6ff3661" = true
+
+# two multi-byte values
+"68694449-25d2-4974-ba75-fa7bb36db212" = true
+
+# many multi-byte values
+"51a06b5c-de1b-4487-9a50-9db1b8930d85" = true
+
+# one byte
+"baa73993-4514-4915-bac0-f7f585e0e59a" = true
+
+# two bytes
+"72e94369-29f9-46f2-8c95-6c5b7a595aee" = true
+
+# three bytes
+"df5a44c4-56f7-464e-a997-1db5f63ce691" = true
+
+# four bytes
+"1bb58684-f2dc-450a-8406-1f3452aa1947" = true
+
+# maximum 32-bit integer
+"cecd5233-49f1-4dd1-a41a-9840a40f09cd" = true
+
+# incomplete sequence causes error
+"e7d74ba3-8b8e-4bcb-858d-d08302e15695" = true
+
+# incomplete sequence causes error, even if value is zero
+"aa378291-9043-4724-bc53-aca1b4a3fcb6" = true
+
+# multiple values
+"a91e6f5a-c64a-48e3-8a75-ce1a81e0ebee" = true
diff --git a/exercises/word-count/.meta/tests.toml b/exercises/word-count/.meta/tests.toml
new file mode 100644
index 00000000..e3b34ac0
--- /dev/null
+++ b/exercises/word-count/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# count one word
+"61559d5f-2cad-48fb-af53-d3973a9ee9ef" = true
+
+# count one of each word
+"5abd53a3-1aed-43a4-a15a-29f88c09cbbd" = true
+
+# multiple occurrences of a word
+"2a3091e5-952e-4099-9fac-8f85d9655c0e" = true
+
+# handles cramped lists
+"e81877ae-d4da-4af4-931c-d923cd621ca6" = true
+
+# handles expanded lists
+"7349f682-9707-47c0-a9af-be56e1e7ff30" = true
+
+# ignore punctuation
+"a514a0f2-8589-4279-8892-887f76a14c82" = true
+
+# include numbers
+"d2e5cee6-d2ec-497b-bdc9-3ebe092ce55e" = true
+
+# normalize case
+"dac6bc6a-21ae-4954-945d-d7f716392dbf" = true
+
+# with apostrophes
+"4185a902-bdb0-4074-864c-f416e42a0f19" = true
+
+# with quotations
+"be72af2b-8afe-4337-b151-b297202e4a7b" = true
+
+# substrings from the beginning
+"8d6815fe-8a51-4a65-96f9-2fb3f6dc6ed6" = true
+
+# multiple spaces not detected as a word
+"c5f4ef26-f3f7-4725-b314-855c04fb4c13" = true
+
+# alternating word separators not detected as a word
+"50176e8a-fe8e-4f4c-b6b6-aa9cf8f20360" = true
diff --git a/exercises/word-search/.meta/tests.toml b/exercises/word-search/.meta/tests.toml
new file mode 100644
index 00000000..c48f27d5
--- /dev/null
+++ b/exercises/word-search/.meta/tests.toml
@@ -0,0 +1,61 @@
+[canonical-tests]
+
+# Should accept an initial game grid and a target search word
+"b4057815-0d01-41f0-9119-6a91f54b2a0a" = true
+
+# Should locate one word written left to right
+"6b22bcc5-6cbf-4674-931b-d2edbff73132" = true
+
+# Should locate the same word written left to right in a different position
+"ff462410-434b-442d-9bc3-3360c75f34a8" = true
+
+# Should locate a different left to right word
+"a02febae-6347-443e-b99c-ab0afb0b8fca" = true
+
+# Should locate that different left to right word in a different position
+"e42e9987-6304-4e13-8232-fa07d5280130" = true
+
+# Should locate a left to right word in two line grid
+"9bff3cee-49b9-4775-bdfb-d55b43a70b2f" = true
+
+# Should locate a left to right word in three line grid
+"851a35fb-f499-4ec1-9581-395a87903a22" = true
+
+# Should locate a left to right word in ten line grid
+"2f3dcf84-ba7d-4b75-8b8d-a3672b32c035" = true
+
+# Should locate that left to right word in a different position in a ten line grid
+"006d4856-f365-4e84-a18c-7d129ce9eefb" = true
+
+# Should locate a different left to right word in a ten line grid
+"eff7ac9f-ff11-443e-9747-40850c12ab60" = true
+
+# Should locate multiple words
+"dea39f86-8c67-4164-8884-13bfc48bd13b" = true
+
+# Should locate a single word written right to left
+"29e6a6a5-f80c-48a6-8e68-05bbbe187a09" = true
+
+# Should locate multiple words written in different horizontal directions
+"3cf34428-b43f-48b6-b332-ea0b8836011d" = true
+
+# Should locate words written top to bottom
+"2c8cd344-a02f-464b-93b6-8bf1bd890003" = true
+
+# Should locate words written bottom to top
+"9ee1e43d-e59d-4c32-9a5f-6a22d4a1550f" = true
+
+# Should locate words written top left to bottom right
+"6a21a676-f59e-4238-8e88-9f81015afae9" = true
+
+# Should locate words written bottom right to top left
+"c9125189-1861-4b0d-a14e-ba5dab29ca7c" = true
+
+# Should locate words written bottom left to top right
+"b19e2149-7fc5-41ec-a8a9-9bc6c6c38c40" = true
+
+# Should locate words written top right to bottom left
+"69e1d994-a6d7-4e24-9b5a-db76751c2ef8" = true
+
+# Should fail to locate a word that is not in the puzzle
+"695531db-69eb-463f-8bad-8de3bf5ef198" = true
diff --git a/exercises/wordy/.meta/tests.toml b/exercises/wordy/.meta/tests.toml
new file mode 100644
index 00000000..c3e3faa2
--- /dev/null
+++ b/exercises/wordy/.meta/tests.toml
@@ -0,0 +1,70 @@
+[canonical-tests]
+
+# just a number
+"88bf4b28-0de3-4883-93c7-db1b14aa806e" = true
+
+# addition
+"bb8c655c-cf42-4dfc-90e0-152fcfd8d4e0" = true
+
+# more addition
+"79e49e06-c5ae-40aa-a352-7a3a01f70015" = true
+
+# addition with negative numbers
+"b345dbe0-f733-44e1-863c-5ae3568f3803" = true
+
+# large addition
+"cd070f39-c4cc-45c4-97fb-1be5e5846f87" = true
+
+# subtraction
+"0d86474a-cd93-4649-a4fa-f6109a011191" = true
+
+# multiplication
+"30bc8395-5500-4712-a0cf-1d788a529be5" = true
+
+# division
+"34c36b08-8605-4217-bb57-9a01472c427f" = true
+
+# multiple additions
+"da6d2ce4-fb94-4d26-8f5f-b078adad0596" = true
+
+# addition and subtraction
+"7fd74c50-9911-4597-be09-8de7f2fea2bb" = true
+
+# multiple subtraction
+"b120ffd5-bad6-4e22-81c8-5512e8faf905" = true
+
+# subtraction then addition
+"4f4a5749-ef0c-4f60-841f-abcfaf05d2ae" = true
+
+# multiple multiplication
+"312d908c-f68f-42c9-aa75-961623cc033f" = true
+
+# addition and multiplication
+"38e33587-8940-4cc1-bc28-bfd7e3966276" = true
+
+# multiple division
+"3c854f97-9311-46e8-b574-92b60d17d394" = true
+
+# unknown operation
+"3ad3e433-8af7-41ec-aa9b-97b42ab49357" = true
+
+# Non math question
+"8a7e85a8-9e7b-4d46-868f-6d759f4648f8" = true
+
+# reject problem missing an operand
+"42d78b5f-dbd7-4cdb-8b30-00f794bb24cf" = true
+
+# reject problem with no operands or operators
+"c2c3cbfc-1a72-42f2-b597-246e617e66f5" = true
+
+# reject two operations in a row
+"4b3df66d-6ed5-4c95-a0a1-d38891fbdab6" = true
+
+# reject two numbers in a row
+"6abd7a50-75b4-4665-aa33-2030fd08bab1" = true
+
+# reject postfix notation
+"10a56c22-e0aa-405f-b1d2-c642d9c4c9de" = true
+
+# reject prefix notation
+"0035bc63-ac43-4bb5-ad6d-e8651b7d954e" = true
diff --git a/exercises/yacht/.meta/tests.toml b/exercises/yacht/.meta/tests.toml
new file mode 100644
index 00000000..459f97a7
--- /dev/null
+++ b/exercises/yacht/.meta/tests.toml
@@ -0,0 +1,85 @@
+[canonical-tests]
+
+# Yacht
+"3060e4a5-4063-4deb-a380-a630b43a84b6" = true
+
+# Not Yacht
+"15026df2-f567-482f-b4d5-5297d57769d9" = true
+
+# Ones
+"36b6af0c-ca06-4666-97de-5d31213957a4" = true
+
+# Ones, out of order
+"023a07c8-6c6e-44d0-bc17-efc5e1b8205a" = true
+
+# No ones
+"7189afac-cccd-4a74-8182-1cb1f374e496" = true
+
+# Twos
+"793c4292-dd14-49c4-9707-6d9c56cee725" = true
+
+# Fours
+"dc41bceb-d0c5-4634-a734-c01b4233a0c6" = true
+
+# Yacht counted as threes
+"f6125417-5c8a-4bca-bc5b-b4b76d0d28c8" = true
+
+# Yacht of 3s counted as fives
+"464fc809-96ed-46e4-acb8-d44e302e9726" = true
+
+# Sixes
+"e8a036e0-9d21-443a-8b5f-e15a9e19a761" = true
+
+# Full house two small, three big
+"51cb26db-6b24-49af-a9ff-12f53b252eea" = true
+
+# Full house three small, two big
+"1822ca9d-f235-4447-b430-2e8cfc448f0c" = true
+
+# Two pair is not a full house
+"b208a3fc-db2e-4363-a936-9e9a71e69c07" = true
+
+# Four of a kind is not a full house
+"b90209c3-5956-445b-8a0b-0ac8b906b1c2" = true
+
+# Yacht is not a full house
+"32a3f4ee-9142-4edf-ba70-6c0f96eb4b0c" = true
+
+# Four of a Kind
+"b286084d-0568-4460-844a-ba79d71d79c6" = true
+
+# Yacht can be scored as Four of a Kind
+"f25c0c90-5397-4732-9779-b1e9b5f612ca" = true
+
+# Full house is not Four of a Kind
+"9f8ef4f0-72bb-401a-a871-cbad39c9cb08" = true
+
+# Little Straight
+"b4743c82-1eb8-4a65-98f7-33ad126905cd" = true
+
+# Little Straight as Big Straight
+"7ac08422-41bf-459c-8187-a38a12d080bc" = true
+
+# Four in order but not a little straight
+"97bde8f7-9058-43ea-9de7-0bc3ed6d3002" = true
+
+# No pairs but not a little straight
+"cef35ff9-9c5e-4fd2-ae95-6e4af5e95a99" = true
+
+# Minimum is 1, maximum is 5, but not a little straight
+"fd785ad2-c060-4e45-81c6-ea2bbb781b9d" = true
+
+# Big Straight
+"35bd74a6-5cf6-431a-97a3-4f713663f467" = true
+
+# Big Straight as little straight
+"87c67e1e-3e87-4f3a-a9b1-62927822b250" = true
+
+# No pairs but not a big straight
+"c1fa0a3a-40ba-4153-a42d-32bc34d2521e" = true
+
+# Choice
+"207e7300-5d10-43e5-afdd-213e3ac8827d" = true
+
+# Yacht as choice
+"b524c0cf-32d2-4b40-8fb3-be3500f3f135" = true
diff --git a/exercises/zebra-puzzle/.meta/tests.toml b/exercises/zebra-puzzle/.meta/tests.toml
new file mode 100644
index 00000000..53a5d8ae
--- /dev/null
+++ b/exercises/zebra-puzzle/.meta/tests.toml
@@ -0,0 +1,7 @@
+[canonical-tests]
+
+# resident who drinks water
+"16efb4e4-8ad7-4d5e-ba96-e5537b66fd42" = true
+
+# resident who owns zebra
+"084d5b8b-24e2-40e6-b008-c800da8cd257" = true
diff --git a/exercises/zipper/.meta/tests.toml b/exercises/zipper/.meta/tests.toml
new file mode 100644
index 00000000..fba800be
--- /dev/null
+++ b/exercises/zipper/.meta/tests.toml
@@ -0,0 +1,40 @@
+[canonical-tests]
+
+# data is retained
+"771c652e-0754-4ef0-945c-0675d12ef1f5" = true
+
+# left, right and value
+"d7dcbb92-47fc-4d01-b81a-df3353bc09ff" = true
+
+# dead end
+"613d8286-b05c-4453-b205-e6f9c5966339" = true
+
+# tree from deep focus
+"dda31af7-1c68-4e29-933a-c9d198d94284" = true
+
+# traversing up from top
+"1e3072a6-f85b-430b-b014-cdb4087e3577" = true
+
+# left, right, and up
+"b8505f6a-aed4-4c2e-824f-a0ed8570d74b" = true
+
+# set_value
+"47df1a27-b709-496e-b381-63a03b82ea5f" = true
+
+# set_value after traversing up
+"16a1f1a8-dbed-456d-95ac-1cbb6093e0ab" = true
+
+# set_left with leaf
+"535a91af-a02e-49cd-8d2c-ecb6e4647174" = true
+
+# set_right with null
+"b3f60c4b-a788-4ffd-be5d-1e69aee61de3" = true
+
+# set_right with subtree
+"e91c221d-7b90-4604-b4ec-46638a673a12" = true
+
+# set_value on deep focus
+"c246be85-6648-4e9c-866f-b08cd495149a" = true
+
+# different paths to same zipper
+"47aa85a0-5240-48a4-9f42-e2ac636710ea" = true