{"thread":{"id":"61874","subject":"[RFC PATCH 0/3] Introduce clar testing framework","startedAt":"2024-07-31T09:04:17Z","lastAt":"2024-09-10T06:20:42Z","messageCount":172,"participants":["Patrick Steinhardt","Junio C Hamano","rsbecker@nexbridge.com","René Scharfe","Josh Steadmon","Phillip Wood","Johannes Schindelin","phillip.wood123@gmail.com","Eric Sunshine"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"499670","messageId":"cover.1722415748.git.ps@pks.im","threadId":"61874","inReplyTo":null,"subject":"[RFC PATCH 0/3] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-07-31T09:04:11Z","receivedAt":"2024-07-31T09:04:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthere's been some discussion around extending our unit testing framework\nto avoid duplication when declaring test functions. Right now, a testing\nfunction has to be declared and then wired up via the test's main\nfunction, which can be a bit annoying. In the thread, René proposes an\nalternative that gets rid of this duplication by using macros. And while\nthat does solve the issue, there were some concerns about things being\ntoo much \"magic\" while at the same time not being flexible enough.\n\nPart of the discussion revolved around whether we maybe want to have a\nproper unit testing framework in our codebase instead of reinventing the\nwheel. As I quite liked the \"clar\" [2] testing framework from back when\nI was still developing libgit2 I proposed it as a possible alternative.\nThis patch series wires up the clar framework as a proof of concept and\nconverts the strvec test suite to use it.\n\nThe magic to avoid the above code duplication is quite self-contained in\na \"generate.py\" file. This script extracts function declarations from\nall unit test suites and then writes those into a \"clar.suite\" header\nfile. All that one needs to do is thus to declare a function with a\nspecific name \"test_<suite>__<name>\" and then everything else gets wired\nup automatically.\n\nWhether this is better than the solution proposed by René is probably a\nmatter of taste. While I'm not a huge fan of the macro-based solution,\nI don't want to veto it either (not that I'd have that power anyway). So\nplease, you should rather read this as a proof of concept to see how\nalternatives could look like such that we have a better picture of where\nwe want to end up.\n\nSome random thoughts:\n\n  - The mandated Python dependency is suboptimal in my opinion.\n    Rewriting the script in e.g. Perl should be easy enough though, it's\n    no rocket science.\n\n  - I prefer that the proposed solution results in a single binary as\n    compared to one binary per test system.\n\n  - The clar gives us the ability to pick which tests to run via command\n    line parameters, which I personally like more than picking the\n    specific binary to run.\n\n  - The clar replaces some test assertions that we already have. They\n    feel a bit more mature, but overall there aren't all that many\n    assertions available. If we wanted to pick it up, then we'd likely\n    have to add some more wrappers.\n\n  - The clar uses longjmp instead of manually having to `return` from\n    functions in case there was an assertion failure. This is easier to\n    work with in my opinion.\n\nAlso, note that I only tested this on my Linux machine. I have no clue\nwhether this works as-is on Windows, but I do know that libgit2 tests\nrun on Linux, macOS and Windows. So it should work in theory, it's just\na matter of polishing this series.\n\nI'm happy to hear your thoughts on this, even if it ultimately ends up\nbeing shot down.\n\nPatrick\n\n[1]: <85b6b8a9-ee5f-42ab-bcbc-49976b30ef33@web.de>\n[2]: https://github.com/clar-test/clar\n\nPatrick Steinhardt (3):\n  t: import the clar unit testing framework\n  Makefile: wire up the clar unit testing framework\n  t/unit-tests: convert strvec tests to use clar\n\n .gitignore                                 |   1 +\n Makefile                                   |  36 +-\n t/Makefile                                 |   1 +\n t/unit-tests/.gitignore                    |   3 +\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 154 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 267 +++++++\n t/unit-tests/clar/test/.gitignore          |   5 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n t/unit-tests/{t-strvec.c => strvec.c}      | 124 ++-\n t/unit-tests/unit-test.c                   |  16 +\n t/unit-tests/unit-test.h                   |   3 +\n 25 files changed, 3041 insertions(+), 84 deletions(-)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\n-- \n2.46.0.dirty\n\n"},{"id":"499671","messageId":"b3db953e88ece99b441993c9248223cc0ecc6be8.1722415748.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-07-31T09:04:15Z","receivedAt":"2024-07-31T09:04:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Import the clar unit testing framework at commit faa8419 (Merge pull\nrequest #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 154 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 267 +++++++\n t/unit-tests/clar/test/.gitignore          |   5 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n 18 files changed, 2941 insertions(+)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n\ndiff --git a/t/unit-tests/clar/.github/workflows/ci.yml b/t/unit-tests/clar/.github/workflows/ci.yml\nnew file mode 100644\nindex 0000000000..b1ac2de460\n--- /dev/null\n+++ b/t/unit-tests/clar/.github/workflows/ci.yml\n@@ -0,0 +1,23 @@\n+name: CI\n+\n+on:\n+  push:\n+    branches: [ main ]\n+  pull_request:\n+    branches: [ main ]\n+\n+jobs:\n+  build:\n+    strategy:\n+      matrix:\n+        os: [ ubuntu-latest, macos-latest ]\n+\n+    runs-on: ${{ matrix.os }}\n+\n+    steps:\n+    - name: Check out\n+      uses: actions/checkout@v2\n+    - name: Build\n+      run: |\n+        cd test\n+        make\ndiff --git a/t/unit-tests/clar/COPYING b/t/unit-tests/clar/COPYING\nnew file mode 100644\nindex 0000000000..8983817f0c\n--- /dev/null\n+++ b/t/unit-tests/clar/COPYING\n@@ -0,0 +1,15 @@\n+ISC License\n+\n+Copyright (c) 2011-2015 Vicent Marti\n+\n+Permission to use, copy, modify, and/or distribute this software for any\n+purpose with or without fee is hereby granted, provided that the above\n+copyright notice and this permission notice appear in all copies.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ndiff --git a/t/unit-tests/clar/README.md b/t/unit-tests/clar/README.md\nnew file mode 100644\nindex 0000000000..a8961c5f10\n--- /dev/null\n+++ b/t/unit-tests/clar/README.md\n@@ -0,0 +1,329 @@\n+Come out and Clar\n+=================\n+\n+In Catalan, \"clar\" means clear, easy to perceive.  Using clar will make it\n+easy to test and make clear the quality of your code.\n+\n+> _Historical note_\n+>\n+> Originally the clar project was named \"clay\" because the word \"test\" has its\n+> roots in the latin word *\"testum\"*, meaning \"earthen pot\", and *\"testa\"*,\n+> meaning \"piece of burned clay\"?\n+>\n+> This is because historically, testing implied melting metal in a pot to\n+> check its quality.  Clay is what tests are made of.\n+\n+## Quick Usage Overview\n+\n+Clar is a minimal C unit testing framework. It's been written to replace the\n+old framework in [libgit2][libgit2], but it's both very versatile and\n+straightforward to use.\n+\n+Can you count to funk?\n+\n+- **Zero: Initialize test directory**\n+\n+    ~~~~ sh\n+    $ mkdir tests\n+    $ cp -r $CLAR_ROOT/clar* tests\n+    $ cp $CLAR_ROOT/test/clar_test.h tests\n+    $ cp $CLAR_ROOT/test/main.c.sample tests/main.c\n+    ~~~~\n+\n+- **One: Write some tests**\n+\n+    File: tests/adding.c:\n+\n+    ~~~~ c\n+    /* adding.c for the \"Adding\" suite */\n+    #include \"clar.h\"\n+\n+    static int *answer;\n+\n+    void test_adding__initialize(void)\n+    {\n+        answer = malloc(sizeof(int));\n+        cl_assert_(answer != NULL, \"No memory left?\");\n+        *answer = 42;\n+    }\n+\n+    void test_adding__cleanup(void)\n+    {\n+        free(answer);\n+    }\n+\n+    void test_adding__make_sure_math_still_works(void)\n+    {\n+        cl_assert_(5 > 3, \"Five should probably be greater than three\");\n+        cl_assert_(-5 < 2, \"Negative numbers are small, I think\");\n+        cl_assert_(*answer == 42, \"The universe is doing OK. And the initializer too.\");\n+    }\n+    ~~~~~\n+\n+- **Two: Build the test executable**\n+\n+    ~~~~ sh\n+    $ cd tests\n+    $ $CLAR_PATH/generate.py .\n+    Written `clar.suite` (1 suites)\n+    $ gcc -I. clar.c main.c adding.c -o testit\n+    ~~~~\n+\n+- **Funk: Funk it.**\n+\n+    ~~~~ sh\n+    $ ./testit\n+    ~~~~\n+\n+## The Clar Test Suite\n+\n+Writing a test suite is pretty straightforward. Each test suite is a `*.c`\n+file with a descriptive name: this encourages modularity.\n+\n+Each test suite has optional initialize and cleanup methods. These methods\n+will be called before and after running **each** test in the suite, even if\n+such test fails. As a rule of thumb, if a test needs a different initializer\n+or cleanup method than another test in the same module, that means it\n+doesn't belong in that module. Keep that in mind when grouping tests\n+together.\n+\n+The `initialize` and `cleanup` methods have the following syntax, with\n+`suitename` being the current suite name, e.g. `adding` for the `adding.c`\n+suite.\n+\n+~~~~ c\n+void test_suitename__initialize(void)\n+{\n+    /* init */\n+}\n+\n+void test_suitename__cleanup(void)\n+{\n+    /* cleanup */\n+}\n+~~~~\n+\n+These methods are encouraged to use static, global variables to store the state\n+that will be used by all tests inside the suite.\n+\n+~~~~ c\n+static git_repository *_repository;\n+\n+void test_status__initialize(void)\n+{\n+    create_tmp_repo(STATUS_REPO);\n+    git_repository_open(_repository, STATUS_REPO);\n+}\n+\n+void test_status__cleanup(void)\n+{\n+    git_repository_close(_repository);\n+    git_path_rm(STATUS_REPO);\n+}\n+\n+void test_status__simple_test(void)\n+{\n+    /* do something with _repository */\n+}\n+~~~~\n+\n+Writing the actual tests is just as straightforward. Tests have the\n+`void test_suitename__test_name(void)` signature, and they should **not**\n+be static. Clar will automatically detect and list them.\n+\n+Tests are run as they appear on their original suites: they have no return\n+value. A test is considered \"passed\" if it doesn't raise any errors. Check\n+the \"Clar API\" section to see the various helper functions to check and\n+raise errors during test execution.\n+\n+__Caution:__ If you use assertions inside of `test_suitename__initialize`,\n+make sure that you do not rely on `__initialize` being completely run\n+inside your `test_suitename__cleanup` function. Otherwise you might\n+encounter ressource cleanup twice.\n+\n+## How does Clar work?\n+\n+To use Clar:\n+\n+1. copy the Clar boilerplate to your test directory\n+2. copy (and probably modify) the sample `main.c` (from\n+   `$CLAR_PATH/test/main.c.sample`)\n+3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and\n+   write out the test suite metadata.\n+4. compile your test files and the Clar boilerplate into a single test\n+   executable\n+5. run the executable to test!\n+\n+The Clar boilerplate gives you a set of useful test assertions and features\n+(like accessing or making sandbox copies of fixture data).  It consists of\n+the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory.\n+You should not need to edit these files.\n+\n+The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes\n+`clar_test(argc, argv)` to run the tests.  Usually, you will edit this file\n+to perform any framework specific initialization and teardown that you need.\n+\n+The Clar mixer (`generate.py`) recursively scans your test directory for\n+any `.c` files, parses them, and writes the `clar.suite` file with all of\n+the metadata about your tests.  When you build, the `clar.suite` file is\n+included into `clar.c`.\n+\n+The mixer can be run with **Python 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and PyPy 1.6**.\n+\n+Commandline usage of the mixer is as follows:\n+\n+    $ ./generate.py .\n+\n+Where `.` is the folder where all the test suites can be found. The mixer\n+will automatically locate all the relevant source files and build the\n+testing metadata. The metadata will be written to `clar.suite`, in the same\n+folder as all the test suites. This file is included by `clar.c` and so\n+must be accessible via `#include` when building the test executable.\n+\n+    $ gcc -I. clar.c main.c suite1.c test2.c -o run_tests\n+\n+**Note that the Clar mixer only needs to be ran when adding new tests to a\n+suite, in order to regenerate the metadata**. As a result, the `clar.suite`\n+file can be checked into version control if you wish to be able to build\n+your test suite without having to re-run the mixer.\n+\n+This is handy when e.g. generating tests in a local computer, and then\n+building and testing them on an embedded device or a platform where Python\n+is not available.\n+\n+### Fixtures\n+\n+Clar can create sandboxed fixtures for you to use in your test. You'll need to compile *clar.c* with an additional `CFLAG`, `-DCLAR_FIXTURE_PATH`. This should be an absolute path to your fixtures directory.\n+\n+Once that's done, you can use the fixture API as defined below.\n+\n+## The Clar API\n+\n+Clar makes the following methods available from all functions in a test\n+suite.\n+\n+-   `cl_must_pass(call)`, `cl_must_pass_(call, message)`: Verify that the given\n+    function call passes, in the POSIX sense (returns a value greater or equal\n+    to 0).\n+\n+-   `cl_must_fail(call)`, `cl_must_fail_(call, message)`: Verify that the given\n+    function call fails, in the POSIX sense (returns a value less than 0).\n+\n+-   `cl_assert(expr)`, `cl_assert_(expr, message)`: Verify that `expr` is true.\n+\n+-   `cl_check_pass(call)`, `cl_check_pass_(call, message)`: Verify that the\n+    given function call passes, in the POSIX sense (returns a value greater or\n+    equal to 0). If the function call doesn't succeed, a test failure will be\n+    logged but the test's execution will continue.\n+\n+-   `cl_check_fail(call)`, `cl_check_fail_(call, message)`: Verify that the\n+    given function call fails, in the POSIX sense (returns a value less than\n+    0). If the function call doesn't fail, a test failure will be logged but\n+    the test's execution will continue.\n+\n+-   `cl_check(expr)`: Verify that `expr` is true. If `expr` is not\n+    true, a test failure will be logged but the test's execution will continue.\n+\n+-   `cl_fail(message)`: Fail the current test with the given message.\n+\n+-   `cl_warning(message)`: Issue a warning. This warning will be\n+    logged as a test failure but the test's execution will continue.\n+\n+-   `cl_set_cleanup(void (*cleanup)(void *), void *opaque)`: Set the cleanup\n+    method for a single test. This method will be called with `opaque` as its\n+    argument before the test returns (even if the test has failed).\n+    If a global cleanup method is also available, the local cleanup will be\n+    called first, and then the global.\n+\n+-   `cl_assert_equal_i(int,int)`: Verify that two integer values are equal.\n+    The advantage of this over a simple `cl_assert` is that it will format\n+    a much nicer error report if the values are not equal.\n+\n+-   `cl_assert_equal_s(const char *,const char *)`: Verify that two strings\n+    are equal.  The expected value can also be NULL and this will correctly\n+    test for that.\n+\n+-   `cl_fixture_sandbox(const char *)`: Sets up a sandbox for a fixture\n+    so that you can mutate the file directly.\n+\n+-   `cl_fixture_cleanup(const char *)`: Tears down the previous fixture\n+    sandbox.\n+\n+-   `cl_fixture(const char *)`: Gets the full path to a fixture file.\n+\n+Please do note that these methods are *always* available whilst running a\n+test, even when calling auxiliary/static functions inside the same file.\n+\n+It's strongly encouraged to perform test assertions in auxiliary methods,\n+instead of returning error values. This is considered good Clar style.\n+\n+Style Example:\n+\n+~~~~ c\n+/*\n+ * Bad style: auxiliary functions return an error code\n+ */\n+\n+static int check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    if (aux == NULL)\n+        return -1;\n+\n+    return strcmp(my_function(aux), str) == 0 ? 0 : -1;\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    cl_must_pass_(\n+        check_string(\"foo\"),\n+        \"String differs after processing\"\n+    );\n+\n+    cl_must_pass_(\n+        check_string(\"bar\"),\n+        \"String differs after processing\"\n+    );\n+}\n+~~~~\n+\n+~~~~ c\n+/*\n+ * Good style: auxiliary functions perform assertions\n+ */\n+\n+static void check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    cl_assert_(\n+        aux != NULL,\n+        \"String processing failed\"\n+    );\n+\n+    cl_assert_(\n+        strcmp(my_function(aux), str) == 0,\n+        \"String differs after processing\"\n+    );\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    check_string(\"foo\");\n+    check_string(\"bar\");\n+}\n+~~~~\n+\n+About Clar\n+==========\n+\n+Clar has been written from scratch by [Vicent Martí](https://github.com/vmg),\n+to replace the old testing framework in [libgit2][libgit2].\n+\n+Do you know what languages are *in* on the SF startup scene? Node.js *and*\n+Latin.  Follow [@vmg](https://www.twitter.com/vmg) on Twitter to\n+receive more lessons on word etymology. You can be hip too.\n+\n+\n+[libgit2]: https://github.com/libgit2/libgit2\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nnew file mode 100644\nindex 0000000000..3fc2c76815\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.c\n@@ -0,0 +1,842 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#include <assert.h>\n+#include <setjmp.h>\n+#include <stdlib.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <math.h>\n+#include <stdarg.h>\n+#include <wchar.h>\n+#include <time.h>\n+\n+/* required for sandboxing */\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#ifdef _WIN32\n+#\tinclude <windows.h>\n+#\tinclude <io.h>\n+#\tinclude <shellapi.h>\n+#\tinclude <direct.h>\n+\n+#\tdefine _MAIN_CC __cdecl\n+\n+#\tifndef stat\n+#\t\tdefine stat(path, st) _stat(path, st)\n+#\tendif\n+#\tifndef mkdir\n+#\t\tdefine mkdir(path, mode) _mkdir(path)\n+#\tendif\n+#\tifndef chdir\n+#\t\tdefine chdir(path) _chdir(path)\n+#\tendif\n+#\tifndef access\n+#\t\tdefine access(path, mode) _access(path, mode)\n+#\tendif\n+#\tifndef strdup\n+#\t\tdefine strdup(str) _strdup(str)\n+#\tendif\n+#\tifndef strcasecmp\n+#\t\tdefine strcasecmp(a,b) _stricmp(a,b)\n+#\tendif\n+\n+#\tifndef __MINGW32__\n+#\t\tpragma comment(lib, \"shell32\")\n+#\t\tifndef strncpy\n+#\t\t\tdefine strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)\n+#\t\tendif\n+#\t\tifndef W_OK\n+#\t\t\tdefine W_OK 02\n+#\t\tendif\n+#\t\tifndef S_ISDIR\n+#\t\t\tdefine S_ISDIR(x) ((x & _S_IFDIR) != 0)\n+#\t\tendif\n+#\t\tdefine p_snprintf(buf,sz,fmt,...) _snprintf_s(buf,sz,_TRUNCATE,fmt,__VA_ARGS__)\n+#\telse\n+#\t\tdefine p_snprintf snprintf\n+#\tendif\n+\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"Iu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"Ix\"\n+#\tendif\n+\n+#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+\ttypedef struct stat STAT_T;\n+#\telse\n+\ttypedef struct _stat STAT_T;\n+#\tendif\n+#else\n+#\tinclude <sys/wait.h> /* waitpid(2) */\n+#\tinclude <unistd.h>\n+#\tdefine _MAIN_CC\n+#\tdefine p_snprintf snprintf\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"zu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"zx\"\n+#\tendif\n+\ttypedef struct stat STAT_T;\n+#endif\n+\n+#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n+\n+#include \"clar.h\"\n+\n+static void fs_rm(const char *_source);\n+static void fs_copy(const char *_source, const char *dest);\n+\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name);\n+#endif\n+\n+struct clar_error {\n+\tconst char *file;\n+\tconst char *function;\n+\tsize_t line_number;\n+\tconst char *error_msg;\n+\tchar *description;\n+\n+\tstruct clar_error *next;\n+};\n+\n+struct clar_explicit {\n+\tsize_t suite_idx;\n+\tconst char *filter;\n+\n+\tstruct clar_explicit *next;\n+};\n+\n+struct clar_report {\n+\tconst char *test;\n+\tint test_number;\n+\tconst char *suite;\n+\n+\tenum cl_test_status status;\n+\ttime_t start;\n+\tdouble elapsed;\n+\n+\tstruct clar_error *errors;\n+\tstruct clar_error *last_error;\n+\n+\tstruct clar_report *next;\n+};\n+\n+struct clar_summary {\n+\tconst char *filename;\n+\tFILE *fp;\n+};\n+\n+static struct {\n+\tenum cl_test_status test_status;\n+\n+\tconst char *active_test;\n+\tconst char *active_suite;\n+\n+\tint total_skipped;\n+\tint total_errors;\n+\n+\tint tests_ran;\n+\tint suites_ran;\n+\n+\tenum cl_output_format output_format;\n+\n+\tint report_errors_only;\n+\tint exit_on_error;\n+\tint verbosity;\n+\n+\tint write_summary;\n+\tchar *summary_filename;\n+\tstruct clar_summary *summary;\n+\n+\tstruct clar_explicit *explicit;\n+\tstruct clar_explicit *last_explicit;\n+\n+\tstruct clar_report *reports;\n+\tstruct clar_report *last_report;\n+\n+\tvoid (*local_cleanup)(void *);\n+\tvoid *local_cleanup_payload;\n+\n+\tjmp_buf trampoline;\n+\tint trampoline_enabled;\n+\n+\tcl_trace_cb *pfn_trace_cb;\n+\tvoid *trace_payload;\n+\n+} _clar;\n+\n+struct clar_func {\n+\tconst char *name;\n+\tvoid (*ptr)(void);\n+};\n+\n+struct clar_suite {\n+\tconst char *name;\n+\tstruct clar_func initialize;\n+\tstruct clar_func cleanup;\n+\tconst struct clar_func *tests;\n+\tsize_t test_count;\n+\tint enabled;\n+};\n+\n+/* From clar_print_*.c */\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n+static void clar_print_onsuite(const char *suite_name, int suite_index);\n+static void clar_print_onabort(const char *msg, ...);\n+\n+/* From clar_sandbox.c */\n+static void clar_unsandbox(void);\n+static int clar_sandbox(void);\n+\n+/* From summary.h */\n+static struct clar_summary *clar_summary_init(const char *filename);\n+static int clar_summary_shutdown(struct clar_summary *fp);\n+\n+/* Load the declarations for the test suite */\n+#include \"clar.suite\"\n+\n+\n+#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n+\t} while (0)\n+\n+void cl_trace_register(cl_trace_cb *cb, void *payload)\n+{\n+\t_clar.pfn_trace_cb = cb;\n+\t_clar.trace_payload = payload;\n+}\n+\n+\n+/* Core test functions */\n+static void\n+clar_report_errors(struct clar_report *report)\n+{\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (error = report->errors; error; error = error->next)\n+\t\tclar_print_error(i++, _clar.last_report, error);\n+}\n+\n+static void\n+clar_report_all(void)\n+{\n+\tstruct clar_report *report;\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (report = _clar.reports; report; report = report->next) {\n+\t\tif (report->status != CL_TEST_FAILURE)\n+\t\t\tcontinue;\n+\n+\t\tfor (error = report->errors; error; error = error->next)\n+\t\t\tclar_print_error(i++, report, error);\n+\t}\n+}\n+\n+#ifdef WIN32\n+# define clar_time DWORD\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\t*out = GetTickCount();\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)*end - (double)*start) / 1000;\n+}\n+#else\n+# include <sys/time.h>\n+\n+# define clar_time struct timeval\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\tstruct timezone tz;\n+\n+\tgettimeofday(out, &tz);\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n+\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n+}\n+#endif\n+\n+static void\n+clar_run_test(\n+\tconst struct clar_suite *suite,\n+\tconst struct clar_func *test,\n+\tconst struct clar_func *initialize,\n+\tconst struct clar_func *cleanup)\n+{\n+\tclar_time start, end;\n+\n+\t_clar.trampoline_enabled = 1;\n+\n+\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n+\n+\t_clar.last_report->start = time(NULL);\n+\tclar_time_now(&start);\n+\n+\tif (setjmp(_clar.trampoline) == 0) {\n+\t\tif (initialize->ptr != NULL)\n+\t\t\tinitialize->ptr();\n+\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n+\t\ttest->ptr();\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n+\t}\n+\n+\tclar_time_now(&end);\n+\n+\t_clar.trampoline_enabled = 0;\n+\n+\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n+\t\t_clar.last_report->status = CL_TEST_OK;\n+\n+\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n+\n+\tif (_clar.local_cleanup != NULL)\n+\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n+\n+\tif (cleanup->ptr != NULL)\n+\t\tcleanup->ptr();\n+\n+\tCL_TRACE(CL_TRACE__TEST__END);\n+\n+\t_clar.tests_ran++;\n+\n+\t/* remove any local-set cleanup methods */\n+\t_clar.local_cleanup = NULL;\n+\t_clar.local_cleanup_payload = NULL;\n+\n+\tif (_clar.report_errors_only) {\n+\t\tclar_report_errors(_clar.last_report);\n+\t} else {\n+\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n+\t}\n+}\n+\n+static void\n+clar_run_suite(const struct clar_suite *suite, const char *filter)\n+{\n+\tconst struct clar_func *test = suite->tests;\n+\tsize_t i, matchlen;\n+\tstruct clar_report *report;\n+\tint exact = 0;\n+\n+\tif (!suite->enabled)\n+\t\treturn;\n+\n+\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\treturn;\n+\n+\tif (!_clar.report_errors_only)\n+\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n+\n+\t_clar.active_suite = suite->name;\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n+\n+\tif (filter) {\n+\t\tsize_t suitelen = strlen(suite->name);\n+\t\tmatchlen = strlen(filter);\n+\t\tif (matchlen <= suitelen) {\n+\t\t\tfilter = NULL;\n+\t\t} else {\n+\t\t\tfilter += suitelen;\n+\t\t\twhile (*filter == ':')\n+\t\t\t\t++filter;\n+\t\t\tmatchlen = strlen(filter);\n+\n+\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n+\t\t\t\texact = 1;\n+\t\t\t\tmatchlen--;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (i = 0; i < suite->test_count; ++i) {\n+\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n+\t\t\tcontinue;\n+\n+\t\tif (exact && strlen(test[i].name) != matchlen)\n+\t\t\tcontinue;\n+\n+\t\t_clar.active_test = test[i].name;\n+\n+\t\treport = calloc(1, sizeof(struct clar_report));\n+\t\treport->suite = _clar.active_suite;\n+\t\treport->test = _clar.active_test;\n+\t\treport->test_number = _clar.tests_ran;\n+\t\treport->status = CL_TEST_NOTRUN;\n+\n+\t\tif (_clar.reports == NULL)\n+\t\t\t_clar.reports = report;\n+\n+\t\tif (_clar.last_report != NULL)\n+\t\t\t_clar.last_report->next = report;\n+\n+\t\t_clar.last_report = report;\n+\n+\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n+\n+\t\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\t\treturn;\n+\t}\n+\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_END);\n+}\n+\n+static void\n+clar_usage(const char *arg)\n+{\n+\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n+\tprintf(\"Options:\\n\");\n+\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n+\tprintf(\"  -iname        Include the suite with `name`\\n\");\n+\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n+\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n+\tprintf(\"  -q            Only report tests that had an error\\n\");\n+\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n+\tprintf(\"  -t            Display results in tap format\\n\");\n+\tprintf(\"  -l            Print suite names\\n\");\n+\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n+\texit(-1);\n+}\n+\n+static void\n+clar_parse_args(int argc, char **argv)\n+{\n+\tint i;\n+\n+\t/* Verify options before execute */\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tif (argument[0] != '-' || argument[1] == '\\0'\n+\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n+\t\t\tclar_usage(argv[0]);\n+\t\t}\n+\t}\n+\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tswitch (argument[1]) {\n+\t\tcase 's':\n+\t\tcase 'i':\n+\t\tcase 'x': { /* given suite name */\n+\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n+\t\t\tchar action = argument[1];\n+\t\t\tsize_t j, arglen, suitelen, cmplen;\n+\n+\t\t\targument += offset;\n+\t\t\targlen = strlen(argument);\n+\n+\t\t\tif (arglen == 0)\n+\t\t\t\tclar_usage(argv[0]);\n+\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n+\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n+\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n+\n+\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n+\t\t\t\t\tint exact = (arglen >= suitelen);\n+\n+\t\t\t\t\t/* Do we have a real suite prefix separated by a\n+\t\t\t\t\t * trailing '::' or just a matching substring? */\n+\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n+\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n+\t\t\t\t\t    continue;\n+\n+\t\t\t\t\t++found;\n+\n+\t\t\t\t\tif (!exact)\n+\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n+\n+\t\t\t\t\tswitch (action) {\n+\t\t\t\t\tcase 's': {\n+\t\t\t\t\t\tstruct clar_explicit *explicit =\n+\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n+\t\t\t\t\t\tassert(explicit);\n+\n+\t\t\t\t\t\texplicit->suite_idx = j;\n+\t\t\t\t\t\texplicit->filter = argument;\n+\n+\t\t\t\t\t\tif (_clar.explicit == NULL)\n+\t\t\t\t\t\t\t_clar.explicit = explicit;\n+\n+\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n+\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n+\n+\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n+\t\t\t\t\t\t_clar.last_explicit = explicit;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n+\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif (exact)\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (!found) {\n+\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n+\t\t\t\texit(-1);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase 'q':\n+\t\t\t_clar.report_errors_only = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 'Q':\n+\t\t\t_clar.exit_on_error = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 't':\n+\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n+\t\t\tbreak;\n+\n+\t\tcase 'l': {\n+\t\t\tsize_t j;\n+\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n+\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n+\n+\t\t\texit(0);\n+\t\t}\n+\n+\t\tcase 'v':\n+\t\t\t_clar.verbosity++;\n+\t\t\tbreak;\n+\n+\t\tcase 'r':\n+\t\t\t_clar.write_summary = 1;\n+\t\t\tfree(_clar.summary_filename);\n+\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tassert(!\"Unexpected commandline argument!\");\n+\t\t}\n+\t}\n+}\n+\n+void\n+clar_test_init(int argc, char **argv)\n+{\n+\tconst char *summary_env;\n+\n+\tif (argc > 1)\n+\t\tclar_parse_args(argc, argv);\n+\n+\tclar_print_init(\n+\t\t(int)_clar_callback_count,\n+\t\t(int)_clar_suite_count,\n+\t\t\"\"\n+\t);\n+\n+\tif (!_clar.summary_filename &&\n+\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n+\t\t_clar.write_summary = 1;\n+\t\t_clar.summary_filename = strdup(summary_env);\n+\t}\n+\n+\tif (_clar.write_summary && !_clar.summary_filename)\n+\t\t_clar.summary_filename = strdup(\"summary.xml\");\n+\n+\tif (_clar.write_summary &&\n+\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n+\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tif (clar_sandbox() < 0) {\n+\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n+\t\texit(-1);\n+\t}\n+}\n+\n+int\n+clar_test_run(void)\n+{\n+\tsize_t i;\n+\tstruct clar_explicit *explicit;\n+\n+\tif (_clar.explicit) {\n+\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n+\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n+\t} else {\n+\t\tfor (i = 0; i < _clar_suite_count; ++i)\n+\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n+\t}\n+\n+\treturn _clar.total_errors;\n+}\n+\n+void\n+clar_test_shutdown(void)\n+{\n+\tstruct clar_explicit *explicit, *explicit_next;\n+\tstruct clar_report *report, *report_next;\n+\n+\tclar_print_shutdown(\n+\t\t_clar.tests_ran,\n+\t\t(int)_clar_suite_count,\n+\t\t_clar.total_errors\n+\t);\n+\n+\tclar_unsandbox();\n+\n+\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n+\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n+\t\texplicit_next = explicit->next;\n+\t\tfree(explicit);\n+\t}\n+\n+\tfor (report = _clar.reports; report; report = report_next) {\n+\t\treport_next = report->next;\n+\t\tfree(report);\n+\t}\n+\n+\tfree(_clar.summary_filename);\n+}\n+\n+int\n+clar_test(int argc, char **argv)\n+{\n+\tint errors;\n+\n+\tclar_test_init(argc, argv);\n+\terrors = clar_test_run();\n+\tclar_test_shutdown();\n+\n+\treturn errors;\n+}\n+\n+static void abort_test(void)\n+{\n+\tif (!_clar.trampoline_enabled) {\n+\t\tclar_print_onabort(\n+\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n+\t\tclar_report_errors(_clar.last_report);\n+\t\texit(-1);\n+\t}\n+\n+\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n+\tlongjmp(_clar.trampoline, -1);\n+}\n+\n+void clar__skip(void)\n+{\n+\t_clar.last_report->status = CL_TEST_SKIP;\n+\t_clar.total_skipped++;\n+\tabort_test();\n+}\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n+\n+\tif (_clar.last_report->errors == NULL)\n+\t\t_clar.last_report->errors = error;\n+\n+\tif (_clar.last_report->last_error != NULL)\n+\t\t_clar.last_report->last_error->next = error;\n+\n+\t_clar.last_report->last_error = error;\n+\n+\terror->file = file;\n+\terror->function = function;\n+\terror->line_number = line;\n+\terror->error_msg = error_msg;\n+\n+\tif (description != NULL)\n+\t\terror->description = strdup(description);\n+\n+\t_clar.total_errors++;\n+\t_clar.last_report->status = CL_TEST_FAILURE;\n+\n+\tif (should_abort)\n+\t\tabort_test();\n+}\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tif (condition)\n+\t\treturn;\n+\n+\tclar__fail(file, function, line, error_msg, description, should_abort);\n+}\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...)\n+{\n+\tva_list args;\n+\tchar buf[4096];\n+\tint is_equal = 1;\n+\n+\tva_start(args, fmt);\n+\n+\tif (!strcmp(\"%s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n+\t\t\t\t\ts1, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n+\t\t\t\t\tlen, s1, len, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n+\t\t\t\t\twcs1, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n+\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n+\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n+\t\tis_equal = (sz1 == sz2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%p\", fmt)) {\n+\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n+\t\tis_equal = (p1 == p2);\n+\t\tif (!is_equal)\n+\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n+\t}\n+\telse {\n+\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n+\t\tis_equal = (i1 == i2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n+\t\t}\n+\t}\n+\n+\tva_end(args);\n+\n+\tif (!is_equal)\n+\t\tclar__fail(file, function, line, err, buf, should_abort);\n+}\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n+{\n+\t_clar.local_cleanup = cleanup;\n+\t_clar.local_cleanup_payload = opaque;\n+}\n+\n+#include \"clar/sandbox.h\"\n+#include \"clar/fixtures.h\"\n+#include \"clar/fs.h\"\n+#include \"clar/print.h\"\n+#include \"clar/summary.h\"\ndiff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\nnew file mode 100644\nindex 0000000000..8c22382bd5\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.h\n@@ -0,0 +1,173 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST_H__\n+#define __CLAR_TEST_H__\n+\n+#include <stdlib.h>\n+\n+enum cl_test_status {\n+\tCL_TEST_OK,\n+\tCL_TEST_FAILURE,\n+\tCL_TEST_SKIP,\n+\tCL_TEST_NOTRUN,\n+};\n+\n+enum cl_output_format {\n+\tCL_OUTPUT_CLAP,\n+\tCL_OUTPUT_TAP,\n+};\n+\n+/** Setup clar environment */\n+void clar_test_init(int argc, char *argv[]);\n+int clar_test_run(void);\n+void clar_test_shutdown(void);\n+\n+/** One shot setup & run */\n+int clar_test(int argc, char *argv[]);\n+\n+const char *clar_sandbox_path(void);\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n+void cl_fs_cleanup(void);\n+\n+/**\n+ * cl_trace_* is a hook to provide a simple global tracing\n+ * mechanism.\n+ *\n+ * The goal here is to let main() provide clar-proper\n+ * with a callback to optionally write log info for\n+ * test operations into the same stream used by their\n+ * actual tests.  This would let them print test names\n+ * and maybe performance data as they choose.\n+ *\n+ * The goal is NOT to alter the flow of control or to\n+ * override test selection/skipping.  (So the callback\n+ * does not return a value.)\n+ *\n+ * The goal is NOT to duplicate the existing\n+ * pass/fail/skip reporting.  (So the callback\n+ * does not accept a status/errorcode argument.)\n+ *\n+ */\n+typedef enum cl_trace_event {\n+\tCL_TRACE__SUITE_BEGIN,\n+\tCL_TRACE__SUITE_END,\n+\tCL_TRACE__TEST__BEGIN,\n+\tCL_TRACE__TEST__END,\n+\tCL_TRACE__TEST__RUN_BEGIN,\n+\tCL_TRACE__TEST__RUN_END,\n+\tCL_TRACE__TEST__LONGJMP,\n+} cl_trace_event;\n+\n+typedef void (cl_trace_cb)(\n+\tcl_trace_event ev,\n+\tconst char *suite_name,\n+\tconst char *test_name,\n+\tvoid *payload);\n+\n+/**\n+ * Register a callback into CLAR to send global trace events.\n+ * Pass NULL to disable.\n+ */\n+void cl_trace_register(cl_trace_cb *cb, void *payload);\n+\n+\n+#ifdef CLAR_FIXTURE_PATH\n+const char *cl_fixture(const char *fixture_name);\n+void cl_fixture_sandbox(const char *fixture_name);\n+void cl_fixture_cleanup(const char *fixture_name);\n+const char *cl_fixture_basename(const char *fixture_name);\n+#endif\n+\n+/**\n+ * Assertion macros with explicit error message\n+ */\n+#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n+#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n+#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n+\n+/**\n+ * Check macros with explicit error message\n+ */\n+#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n+#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n+#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n+\n+/**\n+ * Assertion macros with no error message\n+ */\n+#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n+#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n+#define cl_assert(expr) cl_assert_(expr, NULL)\n+\n+/**\n+ * Check macros with no error message\n+ */\n+#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n+#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n+#define cl_check(expr) cl_check_(expr, NULL)\n+\n+/**\n+ * Forced failure/warning\n+ */\n+#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n+#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n+\n+#define cl_skip() clar__skip()\n+\n+/**\n+ * Typed assertion macros\n+ */\n+#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n+#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n+\n+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n+\n+#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n+#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n+\n+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+\n+#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n+#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n+#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n+\n+#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n+\n+#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n+\n+void clar__skip(void);\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...);\n+\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\nnew file mode 100644\nindex 0000000000..6ec6423484\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fixtures.h\n@@ -0,0 +1,50 @@\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name)\n+{\n+\tstatic char _path[4096];\n+\tsize_t root_len;\n+\n+\troot_len = strlen(base);\n+\tstrncpy(_path, base, sizeof(_path));\n+\n+\tif (_path[root_len - 1] != '/')\n+\t\t_path[root_len++] = '/';\n+\n+\tif (fixture_name[0] == '/')\n+\t\tfixture_name++;\n+\n+\tstrncpy(_path + root_len,\n+\t\tfixture_name,\n+\t\tsizeof(_path) - root_len);\n+\n+\treturn _path;\n+}\n+\n+const char *cl_fixture(const char *fixture_name)\n+{\n+\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n+}\n+\n+void cl_fixture_sandbox(const char *fixture_name)\n+{\n+\tfs_copy(cl_fixture(fixture_name), _clar_path);\n+}\n+\n+const char *cl_fixture_basename(const char *fixture_name)\n+{\n+\tconst char *p;\n+\n+\tfor (p = fixture_name; *p; p++) {\n+\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n+\t\t\tfixture_name = p+1;\n+\t}\n+\n+\treturn fixture_name;\n+}\n+\n+void cl_fixture_cleanup(const char *fixture_name)\n+{\n+\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nnew file mode 100644\nindex 0000000000..a6eda5e5dc\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -0,0 +1,522 @@\n+/*\n+ * By default, use a read/write loop to copy files on POSIX systems.\n+ * On Linux, use sendfile by default as it's slightly faster.  On\n+ * macOS, we avoid fcopyfile by default because it's slightly slower.\n+ */\n+#undef USE_FCOPYFILE\n+#define USE_SENDFILE 1\n+\n+#ifdef _WIN32\n+\n+#ifdef CLAR_WIN32_LONGPATHS\n+# define CLAR_MAX_PATH 4096\n+#else\n+# define CLAR_MAX_PATH MAX_PATH\n+#endif\n+\n+#define RM_RETRY_COUNT\t5\n+#define RM_RETRY_DELAY\t10\n+\n+#ifdef __MINGW32__\n+\n+/* These security-enhanced functions are not available\n+ * in MinGW, so just use the vanilla ones */\n+#define wcscpy_s(a, b, c) wcscpy((a), (c))\n+#define wcscat_s(a, b, c) wcscat((a), (c))\n+\n+#endif /* __MINGW32__ */\n+\n+static int\n+fs__dotordotdot(WCHAR *_tocheck)\n+{\n+\treturn _tocheck[0] == '.' &&\n+\t\t(_tocheck[1] == '\\0' ||\n+\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n+}\n+\n+static int\n+fs_rmdir_rmdir(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\n+\twhile (!RemoveDirectoryW(_wpath)) {\n+\t\t/* Only retry when we have retries remaining, and the\n+\t\t * error was ERROR_DIR_NOT_EMPTY. */\n+\t\tif (retries++ > RM_RETRY_COUNT ||\n+\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n+\t\t\treturn -1;\n+\n+\t\t/* Give whatever has a handle to a child item some time\n+\t\t * to release it before trying again */\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void translate_path(WCHAR *path, size_t path_size)\n+{\n+    size_t path_len, i;\n+\n+    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n+\treturn;\n+\n+    path_len = wcslen(path);\n+    cl_assert(path_size > path_len + 4);\n+\n+    for (i = path_len; i > 0; i--) {\n+\tWCHAR c = path[i - 1];\n+\n+\tif (c == L'/')\n+\t    path[i + 3] = L'\\\\';\n+\telse\n+\t    path[i + 3] = path[i - 1];\n+    }\n+\n+    path[0] = L'\\\\';\n+    path[1] = L'\\\\';\n+    path[2] = L'?';\n+    path[3] = L'\\\\';\n+    path[path_len + 4] = L'\\0';\n+}\n+\n+static void\n+fs_rmdir_helper(WCHAR *_wsource)\n+{\n+\tWCHAR buffer[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buffer_prefix_len;\n+\n+\t/* Set up the buffer and capture the length */\n+\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n+\ttranslate_path(buffer, CLAR_MAX_PATH);\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n+\tbuffer_prefix_len = wcslen(buffer);\n+\n+\t/* FindFirstFile needs a wildcard to match multiple items */\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buffer, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_rmdir_helper(buffer);\n+\t\telse {\n+\t\t\t/* If set, the +R bit must be cleared before deleting */\n+\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n+\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\t\tcl_assert(DeleteFileW(buffer));\n+\t\t}\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+\n+\t/* Now that the directory is empty, remove it */\n+\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n+}\n+\n+static int\n+fs_rm_wait(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\tDWORD last_error;\n+\n+\tdo {\n+\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n+\t\t\tlast_error = GetLastError();\n+\t\telse\n+\t\t\tlast_error = ERROR_SUCCESS;\n+\n+\t\t/* Is the item gone? */\n+\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n+\t\t\tERROR_PATH_NOT_FOUND == last_error)\n+\t\t\treturn 0;\n+\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n+\t}\n+\twhile (retries++ <= RM_RETRY_COUNT);\n+\n+\treturn -1;\n+}\n+\n+static void\n+fs_rm(const char *_source)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH];\n+\tDWORD attrs;\n+\n+\t/* The input path is UTF-8. Convert it to wide characters\n+\t * for use with the Windows API */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1, /* Indicates NULL termination */\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\n+\t/* Does the item exist? If not, we have no work to do */\n+\tattrs = GetFileAttributesW(wsource);\n+\n+\tif (INVALID_FILE_ATTRIBUTES == attrs)\n+\t\treturn;\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n+\t\tfs_rmdir_helper(wsource);\n+\telse {\n+\t\t/* The item is a file. Strip the +R bit */\n+\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n+\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\tcl_assert(DeleteFileW(wsource));\n+\t}\n+\n+\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n+\tcl_assert(0 == fs_rm_wait(wsource));\n+}\n+\n+static void\n+fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n+{\n+\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n+\n+\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_source, CLAR_MAX_PATH);\n+\tbuf_source_prefix_len = wcslen(buf_source);\n+\n+\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n+\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n+\tbuf_dest_prefix_len = wcslen(buf_dest);\n+\n+\t/* Get an enumerator for the items in the source. */\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buf_source, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\t/* Create the target directory. */\n+\tcl_assert(CreateDirectoryW(_wdest, NULL));\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n+\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_copydir_helper(buf_source, buf_dest);\n+\t\telse\n+\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+}\n+\n+static void\n+fs_copy(const char *_source, const char *_dest)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n+\tDWORD source_attrs, dest_attrs;\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\n+\t/* The input paths are UTF-8. Convert them to wide characters\n+\t * for use with the Windows API. */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1,\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_dest,\n+\t\t\t\t-1,\n+\t\t\t\twdest,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\ttranslate_path(wdest, CLAR_MAX_PATH);\n+\n+\t/* Check the source for existence */\n+\tsource_attrs = GetFileAttributesW(wsource);\n+\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n+\n+\t/* Check the target for existence */\n+\tdest_attrs = GetFileAttributesW(wdest);\n+\n+\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n+\t\t/* Target exists; append last path part of source to target.\n+\t\t * Use FindFirstFile to parse the path */\n+\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n+\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n+\t\tFindClose(find_handle);\n+\n+\t\t/* Check the new target for existence */\n+\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n+\t}\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n+\t\tfs_copydir_helper(wsource, wdest);\n+\telse\n+\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+#ifdef CLAR_FIXTURE_PATH\n+\tfs_rm(fixture_path(_clar_path, \"*\"));\n+#endif\n+}\n+\n+#else\n+\n+#include <errno.h>\n+#include <string.h>\n+#include <limits.h>\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#if defined(__linux__)\n+# include <sys/sendfile.h>\n+#endif\n+\n+#if defined(__APPLE__)\n+# include <copyfile.h>\n+#endif\n+\n+static void basename_r(const char **out, int *out_len, const char *in)\n+{\n+\tsize_t in_len = strlen(in), start_pos;\n+\n+\tfor (in_len = strlen(in); in_len; in_len--) {\n+\t\tif (in[in_len - 1] != '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tfor (start_pos = in_len; start_pos; start_pos--) {\n+\t\tif (in[start_pos - 1] == '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tcl_assert(in_len - start_pos < INT_MAX);\n+\n+\tif (in_len - start_pos > 0) {\n+\t\t*out = &in[start_pos];\n+\t\t*out_len = (in_len - start_pos);\n+\t} else {\n+\t\t*out = \"/\";\n+\t\t*out_len = 1;\n+\t}\n+}\n+\n+static char *joinpath(const char *dir, const char *base, int base_len)\n+{\n+\tchar *out;\n+\tint len;\n+\n+\tif (base_len == -1) {\n+\t\tsize_t bl = strlen(base);\n+\n+\t\tcl_assert(bl < INT_MAX);\n+\t\tbase_len = (int)bl;\n+\t}\n+\n+\tlen = strlen(dir) + base_len + 2;\n+\tcl_assert(len > 0);\n+\n+\tcl_assert(out = malloc(len));\n+\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n+\n+\treturn out;\n+}\n+\n+static void\n+fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n+{\n+\tDIR *source_dir;\n+\tstruct dirent *d;\n+\n+\tmkdir(dest, dest_mode);\n+\n+\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n+\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(source, d->d_name, -1);\n+\t\tfs_copy(child, dest);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\n+\tclosedir(source_dir);\n+}\n+\n+static void\n+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n+{\n+\tint in, out;\n+\n+\tcl_must_pass((in = open(source, O_RDONLY)));\n+\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n+\n+#if USE_FCOPYFILE && defined(__APPLE__)\n+\t((void)(source_len)); /* unused */\n+\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n+#elif USE_SENDFILE && defined(__linux__)\n+\t{\n+\t\tssize_t ret = 0;\n+\n+\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n+\t\t\tsource_len -= (size_t)ret;\n+\t\t}\n+\t\tcl_assert(ret >= 0);\n+\t}\n+#else\n+\t{\n+\t\tchar buf[131072];\n+\t\tssize_t ret;\n+\n+\t\t((void)(source_len)); /* unused */\n+\n+\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n+\t\t\tsize_t len = (size_t)ret;\n+\n+\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n+\t\t\t\tcl_assert(ret <= (ssize_t)len);\n+\t\t\t\tlen -= ret;\n+\t\t\t}\n+\t\t\tcl_assert(ret >= 0);\n+\t\t}\n+\t\tcl_assert(ret == 0);\n+\t}\n+#endif\n+\n+\tclose(in);\n+\tclose(out);\n+}\n+\n+static void\n+fs_copy(const char *source, const char *_dest)\n+{\n+\tchar *dbuf = NULL;\n+\tconst char *dest = NULL;\n+\tstruct stat source_st, dest_st;\n+\n+\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n+\n+\tif (lstat(_dest, &dest_st) == 0) {\n+\t\tconst char *base;\n+\t\tint base_len;\n+\n+\t\t/* Target exists and is directory; append basename */\n+\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n+\n+\t\tbasename_r(&base, &base_len, source);\n+\t\tcl_assert(base_len < INT_MAX);\n+\n+\t\tdbuf = joinpath(_dest, base, base_len);\n+\t\tdest = dbuf;\n+\t} else if (errno != ENOENT) {\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t} else {\n+\t\tdest = _dest;\n+\t}\n+\n+\tif (S_ISDIR(source_st.st_mode)) {\n+\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n+\t} else {\n+\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n+\t}\n+\n+\tfree(dbuf);\n+}\n+\n+static void\n+fs_rmdir_helper(const char *path)\n+{\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n+\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(path, d->d_name, -1);\n+\t\tfs_rm(child);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\tclosedir(dir);\n+\n+\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n+}\n+\n+static void\n+fs_rm(const char *path)\n+{\n+\tstruct stat st;\n+\n+\tif (lstat(path, &st)) {\n+\t\tif (errno == ENOENT)\n+\t\t\treturn;\n+\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t}\n+\n+\tif (S_ISDIR(st.st_mode)) {\n+\t\tfs_rmdir_helper(path);\n+\t} else {\n+\t\tcl_must_pass(unlink(path));\n+\t}\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+\tclar_unsandbox();\n+\tclar_sandbox();\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\nnew file mode 100644\nindex 0000000000..c17e2f693b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/print.h\n@@ -0,0 +1,211 @@\n+/* clap: clar protocol, the traditional clar output format */\n+\n+static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n+\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n+}\n+\n+static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"\\n\\n\");\n+\tclar_report_all();\n+}\n+\n+static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tprintf(\"  %d) Failure:\\n\", num);\n+\n+\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n+\t\treport->suite,\n+\t\treport->test,\n+\t\terror->file,\n+\t\terror->line_number);\n+\n+\tprintf(\"  %s\\n\", error->error_msg);\n+\n+\tif (error->description != NULL)\n+\t\tprintf(\"  %s\\n\", error->description);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tif (_clar.verbosity > 1) {\n+\t\tprintf(\"%s::%s: \", suite_name, test_name);\n+\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n+\t\t}\n+\t} else {\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\".\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n+\t\t}\n+\n+\t\tfflush(stdout);\n+\t}\n+}\n+\n+static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tif (_clar.verbosity == 1)\n+\t\tprintf(\"\\n%s\", suite_name);\n+\n+\t(void)suite_index;\n+}\n+\n+static void clar_print_clap_onabort(const char *fmt, va_list arg)\n+{\n+\tvfprintf(stderr, fmt, arg);\n+}\n+\n+/* tap: test anywhere protocol format */\n+\n+static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)suite_names;\n+\tprintf(\"TAP version 13\\n\");\n+}\n+\n+static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"1..%d\\n\", test_count);\n+}\n+\n+static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\t(void)num;\n+\t(void)report;\n+\t(void)error;\n+}\n+\n+static void print_escaped(const char *str)\n+{\n+\tchar *c;\n+\n+\twhile ((c = strchr(str, '\\'')) != NULL) {\n+\t\tprintf(\"%.*s\", (int)(c - str), str);\n+\t\tprintf(\"''\");\n+\t\tstr = c + 1;\n+\t}\n+\n+\tprintf(\"%s\", str);\n+}\n+\n+static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tconst struct clar_error *error = _clar.last_report->errors;\n+\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tswitch(status) {\n+\tcase CL_TEST_OK:\n+\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\tcase CL_TEST_FAILURE:\n+\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\n+\t\tprintf(\"    ---\\n\");\n+\t\tprintf(\"    reason: |\\n\");\n+\t\tprintf(\"      %s\\n\", error->error_msg);\n+\n+\t\tif (error->description)\n+\t\t\tprintf(\"      %s\\n\", error->description);\n+\n+\t\tprintf(\"    at:\\n\");\n+\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n+\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n+\t\tprintf(\"      function: '%s'\\n\", error->function);\n+\t\tprintf(\"    ---\\n\");\n+\n+\t\tbreak;\n+\tcase CL_TEST_SKIP:\n+\tcase CL_TEST_NOTRUN:\n+\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\t}\n+\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n+}\n+\n+static void clar_print_tap_onabort(const char *fmt, va_list arg)\n+{\n+\tprintf(\"Bail out! \");\n+\tvprintf(fmt, arg);\n+\tfflush(stdout);\n+}\n+\n+/* indirection between protocol output selection */\n+\n+#define PRINT(FN, ...) do { \\\n+\t\tswitch (_clar.output_format) { \\\n+\t\t\tcase CL_OUTPUT_CLAP: \\\n+\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tcase CL_OUTPUT_TAP: \\\n+\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tdefault: \\\n+\t\t\t\tabort(); \\\n+\t\t} \\\n+\t} while (0)\n+\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\tPRINT(init, test_count, suite_count, suite_names);\n+}\n+\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\tPRINT(shutdown, test_count, suite_count, error_count);\n+}\n+\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tPRINT(error, num, report, error);\n+}\n+\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tPRINT(ontest, suite_name, test_name, test_number, status);\n+}\n+\n+static void clar_print_onsuite(const char *suite_name, int suite_index)\n+{\n+\tPRINT(onsuite, suite_name, suite_index);\n+}\n+\n+static void clar_print_onabort(const char *msg, ...)\n+{\n+\tva_list argp;\n+\tva_start(argp, msg);\n+\tPRINT(onabort, msg, argp);\n+\tva_end(argp);\n+}\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nnew file mode 100644\nindex 0000000000..0ba1479620\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -0,0 +1,154 @@\n+#ifdef __APPLE__\n+#include <sys/syslimits.h>\n+#endif\n+\n+static char _clar_path[4096 + 1];\n+\n+static int\n+is_valid_tmp_path(const char *path)\n+{\n+\tSTAT_T st;\n+\n+\tif (stat(path, &st) != 0)\n+\t\treturn 0;\n+\n+\tif (!S_ISDIR(st.st_mode))\n+\t\treturn 0;\n+\n+\treturn (access(path, W_OK) == 0);\n+}\n+\n+static int\n+find_tmp_path(char *buffer, size_t length)\n+{\n+#ifndef _WIN32\n+\tstatic const size_t var_count = 5;\n+\tstatic const char *env_vars[] = {\n+\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n+ \t};\n+\n+ \tsize_t i;\n+\n+\tfor (i = 0; i < var_count; ++i) {\n+\t\tconst char *env = getenv(env_vars[i]);\n+\t\tif (!env)\n+\t\t\tcontinue;\n+\n+\t\tif (is_valid_tmp_path(env)) {\n+#ifdef __APPLE__\n+\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n+\t\t\t\treturn 0;\n+#endif\n+\t\t\tstrncpy(buffer, env, length - 1);\n+\t\t\tbuffer[length - 1] = '\\0';\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\t/* If the environment doesn't say anything, try to use /tmp */\n+\tif (is_valid_tmp_path(\"/tmp\")) {\n+#ifdef __APPLE__\n+\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n+\t\t\treturn 0;\n+#endif\n+\t\tstrncpy(buffer, \"/tmp\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+#else\n+\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n+\tif (env_len > 0 && env_len < (DWORD)length)\n+\t\treturn 0;\n+\n+\tif (GetTempPath((DWORD)length, buffer))\n+\t\treturn 0;\n+#endif\n+\n+\t/* This system doesn't like us, try to use the current directory */\n+\tif (is_valid_tmp_path(\".\")) {\n+\t\tstrncpy(buffer, \".\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static void clar_unsandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0')\n+\t\treturn;\n+\n+\tcl_must_pass(chdir(\"..\"));\n+\n+\tfs_rm(_clar_path);\n+}\n+\n+static int build_sandbox_path(void)\n+{\n+#ifdef CLAR_TMPDIR\n+\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n+#else\n+\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n+#endif\n+\n+\tsize_t len;\n+\n+\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n+\t\treturn -1;\n+\n+\tlen = strlen(_clar_path);\n+\n+#ifdef _WIN32\n+\t{ /* normalize path to POSIX forward slashes */\n+\t\tsize_t i;\n+\t\tfor (i = 0; i < len; ++i) {\n+\t\t\tif (_clar_path[i] == '\\\\')\n+\t\t\t\t_clar_path[i] = '/';\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (_clar_path[len - 1] != '/') {\n+\t\t_clar_path[len++] = '/';\n+\t}\n+\n+\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n+\n+#if defined(__MINGW32__)\n+\tif (_mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(_WIN32)\n+\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#else\n+\tif (mkdtemp(_clar_path) == NULL)\n+\t\treturn -1;\n+#endif\n+\n+\treturn 0;\n+}\n+\n+static int clar_sandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n+\t\treturn -1;\n+\n+\tif (chdir(_clar_path) != 0)\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const char *clar_sandbox_path(void)\n+{\n+\treturn _clar_path;\n+}\n+\ndiff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\nnew file mode 100644\nindex 0000000000..4dd352e28b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/summary.h\n@@ -0,0 +1,143 @@\n+\n+#include <stdio.h>\n+#include <time.h>\n+\n+static int clar_summary_close_tag(\n+    struct clar_summary *summary, const char *tag, int indent)\n+{\n+\tconst char *indt;\n+\n+\tif (indent == 0) indt = \"\";\n+\telse if (indent == 1) indt = \"\\t\";\n+\telse indt = \"\\t\\t\";\n+\n+\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n+}\n+\n+static int clar_summary_testsuites(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n+}\n+\n+static int clar_summary_testsuite(struct clar_summary *summary,\n+    int idn, const char *name, time_t timestamp,\n+    int test_count, int fail_count, int error_count)\n+{\n+\tstruct tm *tm = localtime(&timestamp);\n+\tchar iso_dt[20];\n+\n+\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n+\t\treturn -1;\n+\n+\treturn fprintf(summary->fp, \"\\t<testsuite\"\n+\t\t       \" id=\\\"%d\\\"\"\n+\t\t       \" name=\\\"%s\\\"\"\n+\t\t       \" hostname=\\\"localhost\\\"\"\n+\t\t       \" timestamp=\\\"%s\\\"\"\n+\t\t       \" tests=\\\"%d\\\"\"\n+\t\t       \" failures=\\\"%d\\\"\"\n+\t\t       \" errors=\\\"%d\\\">\\n\",\n+\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n+}\n+\n+static int clar_summary_testcase(struct clar_summary *summary,\n+    const char *name, const char *classname, double elapsed)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n+\t\tname, classname, elapsed);\n+}\n+\n+static int clar_summary_failure(struct clar_summary *summary,\n+    const char *type, const char *message, const char *desc)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n+\t    type, message, desc);\n+}\n+\n+static int clar_summary_skipped(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n+}\n+\n+struct clar_summary *clar_summary_init(const char *filename)\n+{\n+\tstruct clar_summary *summary;\n+\tFILE *fp;\n+\n+\tif ((fp = fopen(filename, \"w\")) == NULL) {\n+\t\tperror(\"fopen\");\n+\t\treturn NULL;\n+\t}\n+\n+\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n+\t\tperror(\"malloc\");\n+\t\tfclose(fp);\n+\t\treturn NULL;\n+\t}\n+\n+\tsummary->filename = filename;\n+\tsummary->fp = fp;\n+\n+\treturn summary;\n+}\n+\n+int clar_summary_shutdown(struct clar_summary *summary)\n+{\n+\tstruct clar_report *report;\n+\tconst char *last_suite = NULL;\n+\n+\tif (clar_summary_testsuites(summary) < 0)\n+\t\tgoto on_error;\n+\n+\treport = _clar.reports;\n+\twhile (report != NULL) {\n+\t\tstruct clar_error *error = report->errors;\n+\n+\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n+\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\n+\t\tlast_suite = report->suite;\n+\n+\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n+\n+\t\twhile (error != NULL) {\n+\t\t\tif (clar_summary_failure(summary, \"assert\",\n+\t\t\t    error->error_msg, error->description) < 0)\n+\t\t\t\tgoto on_error;\n+\n+\t\t\terror = error->next;\n+\t\t}\n+\n+\t\tif (report->status == CL_TEST_SKIP)\n+\t\t\tclar_summary_skipped(summary);\n+\n+\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n+\t\t\tgoto on_error;\n+\n+\t\treport = report->next;\n+\n+\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\t}\n+\n+\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n+\t    fclose(summary->fp) != 0)\n+\t\tgoto on_error;\n+\n+\tprintf(\"written summary file to %s\\n\", summary->filename);\n+\n+\tfree(summary);\n+\treturn 0;\n+\n+on_error:\n+\tfclose(summary->fp);\n+\tfree(summary);\n+\treturn -1;\n+}\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nnew file mode 100755\nindex 0000000000..931b4d60d9\n--- /dev/null\n+++ b/t/unit-tests/clar/generate.py\n@@ -0,0 +1,267 @@\n+#!/usr/bin/env python\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+from __future__ import with_statement\n+from string import Template\n+import re, fnmatch, os, sys, codecs, pickle\n+\n+class Module(object):\n+    class Template(object):\n+        def __init__(self, module):\n+            self.module = module\n+\n+        def _render_callback(self, cb):\n+            if not cb:\n+                return '    { NULL, NULL }'\n+            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n+\n+    class DeclarationTemplate(Template):\n+        def render(self):\n+            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n+\n+            for initializer in self.module.initializers:\n+                out += \"extern %s;\\n\" % initializer['declaration']\n+\n+            if self.module.cleanup:\n+                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n+\n+            return out\n+\n+    class CallbacksTemplate(Template):\n+        def render(self):\n+            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n+            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n+            out += \"\\n};\\n\"\n+            return out\n+\n+    class InfoTemplate(Template):\n+        def render(self):\n+            templates = []\n+\n+            initializers = self.module.initializers\n+            if len(initializers) == 0:\n+                initializers = [ None ]\n+\n+            for initializer in initializers:\n+                name = self.module.clean_name()\n+                if initializer and initializer['short_name'].startswith('initialize_'):\n+                    variant = initializer['short_name'][len('initialize_'):]\n+                    name += \" (%s)\" % variant.replace('_', ' ')\n+\n+                template = Template(\n+            r\"\"\"\n+    {\n+        \"${clean_name}\",\n+    ${initialize},\n+    ${cleanup},\n+        ${cb_ptr}, ${cb_count}, ${enabled}\n+    }\"\"\"\n+                ).substitute(\n+                    clean_name = name,\n+                    initialize = self._render_callback(initializer),\n+                    cleanup = self._render_callback(self.module.cleanup),\n+                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n+                    cb_count = len(self.module.callbacks),\n+                    enabled = int(self.module.enabled)\n+                )\n+                templates.append(template)\n+\n+            return ','.join(templates)\n+\n+    def __init__(self, name):\n+        self.name = name\n+\n+        self.mtime = None\n+        self.enabled = True\n+        self.modified = False\n+\n+    def clean_name(self):\n+        return self.name.replace(\"_\", \"::\")\n+\n+    def _skip_comments(self, text):\n+        SKIP_COMMENTS_REGEX = re.compile(\n+            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n+            re.DOTALL | re.MULTILINE)\n+\n+        def _replacer(match):\n+            s = match.group(0)\n+            return \"\" if s.startswith('/') else s\n+\n+        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n+\n+    def parse(self, contents):\n+        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n+\n+        contents = self._skip_comments(contents)\n+        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n+\n+        self.callbacks = []\n+        self.initializers = []\n+        self.cleanup = None\n+\n+        for (declaration, symbol, short_name) in regex.findall(contents):\n+            data = {\n+                \"short_name\" : short_name,\n+                \"declaration\" : declaration,\n+                \"symbol\" : symbol\n+            }\n+\n+            if short_name.startswith('initialize'):\n+                self.initializers.append(data)\n+            elif short_name == 'cleanup':\n+                self.cleanup = data\n+            else:\n+                self.callbacks.append(data)\n+\n+        return self.callbacks != []\n+\n+    def refresh(self, path):\n+        self.modified = False\n+\n+        try:\n+            st = os.stat(path)\n+\n+            # Not modified\n+            if st.st_mtime == self.mtime:\n+                return True\n+\n+            self.modified = True\n+            self.mtime = st.st_mtime\n+\n+            with codecs.open(path, encoding='utf-8') as fp:\n+                raw_content = fp.read()\n+\n+        except IOError:\n+            return False\n+\n+        return self.parse(raw_content)\n+\n+class TestSuite(object):\n+\n+    def __init__(self, path, output):\n+        self.path = path\n+        self.output = output\n+\n+    def should_generate(self, path):\n+        if not os.path.isfile(path):\n+            return True\n+\n+        if any(module.modified for module in self.modules.values()):\n+            return True\n+\n+        return False\n+\n+    def find_modules(self):\n+        modules = []\n+        for root, _, files in os.walk(self.path):\n+            module_root = root[len(self.path):]\n+            module_root = [c for c in module_root.split(os.sep) if c]\n+\n+            tests_in_module = fnmatch.filter(files, \"*.c\")\n+\n+            for test_file in tests_in_module:\n+                full_path = os.path.join(root, test_file)\n+                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n+\n+                modules.append((full_path, module_name))\n+\n+        return modules\n+\n+    def load_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        cache = {}\n+\n+        try:\n+            fp = open(path, 'rb')\n+            cache = pickle.load(fp)\n+            fp.close()\n+        except (IOError, ValueError):\n+            pass\n+\n+        return cache\n+\n+    def save_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        with open(path, 'wb') as cache:\n+            pickle.dump(self.modules, cache)\n+\n+    def load(self, force = False):\n+        module_data = self.find_modules()\n+        self.modules = {} if force else self.load_cache()\n+\n+        for path, name in module_data:\n+            if name not in self.modules:\n+                self.modules[name] = Module(name)\n+\n+            if not self.modules[name].refresh(path):\n+                del self.modules[name]\n+\n+    def disable(self, excluded):\n+        for exclude in excluded:\n+            for module in self.modules.values():\n+                name = module.clean_name()\n+                if name.startswith(exclude):\n+                    module.enabled = False\n+                    module.modified = True\n+\n+    def suite_count(self):\n+        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n+\n+    def callback_count(self):\n+        return sum(len(module.callbacks) for module in self.modules.values())\n+\n+    def write(self):\n+        output = os.path.join(self.output, 'clar.suite')\n+\n+        if not self.should_generate(output):\n+            return False\n+\n+        with open(output, 'w') as data:\n+            modules = sorted(self.modules.values(), key=lambda module: module.name)\n+\n+            for module in modules:\n+                t = Module.DeclarationTemplate(module)\n+                data.write(t.render())\n+\n+            for module in modules:\n+                t = Module.CallbacksTemplate(module)\n+                data.write(t.render())\n+\n+            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n+                Module.InfoTemplate(module).render() for module in modules\n+            ) + \"\\n};\\n\"\n+\n+            data.write(suites)\n+\n+            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n+            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n+\n+        self.save_cache()\n+        return True\n+\n+if __name__ == '__main__':\n+    from optparse import OptionParser\n+\n+    parser = OptionParser()\n+    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n+    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n+    parser.add_option('-o', '--output', dest='output')\n+\n+    options, args = parser.parse_args()\n+    if len(args) > 1:\n+        print(\"More than one path given\")\n+        sys.exit(1)\n+\n+    path = args.pop() if args else '.'\n+    output = options.output or path\n+    suite = TestSuite(path, output)\n+    suite.load(options.force)\n+    suite.disable(options.excluded)\n+    if suite.write():\n+        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n+\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nnew file mode 100644\nindex 0000000000..3e8ae0a3a5\n--- /dev/null\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -0,0 +1,5 @@\n+clar.suite\n+.clarcache\n+clar_test\n+*.o\n+\ndiff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\nnew file mode 100644\nindex 0000000000..93c6b2ad32\n--- /dev/null\n+++ b/t/unit-tests/clar/test/Makefile\n@@ -0,0 +1,39 @@\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+#\n+# Set up the path to the clar sources and to the fixtures directory\n+#\n+# The fixture path needs to be an absolute path so it can be used\n+# even after we have chdir'ed into the test directory while testing.\n+#\n+CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n+TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n+CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n+CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n+\n+CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n+\n+.PHONY: clean\n+\n+# list the objects that go into our test\n+objects = main.o sample.o\n+\n+# build the test executable itself\n+clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n+\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n+\n+# test object files depend on clar macros\n+$(objects) : $(CLAR_PATH)clar.h\n+\n+# build the clar.suite file of test metadata\n+clar.suite:\n+\tpython \"$(CLAR_PATH)generate.py\" .\n+\n+# remove all generated files\n+clean:\n+\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\ndiff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\nnew file mode 100644\nindex 0000000000..0fcaa639aa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/clar_test.h\n@@ -0,0 +1,16 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST__\n+#define __CLAR_TEST__\n+\n+/* Import the standard clar helper functions */\n+#include \"clar.h\"\n+\n+/* Your custom shared includes / defines here */\n+extern int global_test_counter;\n+\n+#endif\ndiff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\nnew file mode 100644\nindex 0000000000..59e56ad255\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c\n@@ -0,0 +1,40 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Sample main() for clar tests.\n+ *\n+ * You should write your own main routine for clar tests that does specific\n+ * setup and teardown as necessary for your application.  The only required\n+ * line is the call to `clar_test(argc, argv)`, which will execute the test\n+ * suite.  If you want to check the return value of the test application,\n+ * your main() should return the same value returned by clar_test().\n+ */\n+\n+int global_test_counter = 0;\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\tint ret;\n+\n+\t/* Your custom initialization here */\n+\tglobal_test_counter = 0;\n+\n+\t/* Run the test suite */\n+\tret = clar_test(argc, argv);\n+\n+\t/* Your custom cleanup here */\n+\tcl_assert_equal_i(8, global_test_counter);\n+\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\nnew file mode 100644\nindex 0000000000..a4d91b72fa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c.sample\n@@ -0,0 +1,27 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Minimal main() for clar tests.\n+ *\n+ * Modify this with any application specific setup or teardown that you need.\n+ * The only required line is the call to `clar_test(argc, argv)`, which will\n+ * execute the test suite.  If you want to check the return value of the test\n+ * application, main() should return the same value returned by clar_test().\n+ */\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\t/* Run the test suite */\n+\treturn clar_test(argc, argv);\n+}\ndiff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\nnew file mode 100644\nindex 0000000000..220f4aa98a\n--- /dev/null\n+++ b/t/unit-tests/clar/test/resources/test/file\n@@ -0,0 +1 @@\n+File\ndiff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\nnew file mode 100644\nindex 0000000000..faa1209262\n--- /dev/null\n+++ b/t/unit-tests/clar/test/sample.c\n@@ -0,0 +1,84 @@\n+#include \"clar_test.h\"\n+#include <sys/stat.h>\n+\n+static int file_size(const char *filename)\n+{\n+\tstruct stat st;\n+\n+\tif (stat(filename, &st) == 0)\n+\t\treturn (int)st.st_size;\n+\treturn -1;\n+}\n+\n+void test_sample__initialize(void)\n+{\n+\tglobal_test_counter++;\n+}\n+\n+void test_sample__cleanup(void)\n+{\n+\tcl_fixture_cleanup(\"test\");\n+\n+\tcl_assert(file_size(\"test/file\") == -1);\n+}\n+\n+void test_sample__1(void)\n+{\n+\tcl_assert(1);\n+\tcl_must_pass(0);  /* 0 == success */\n+\tcl_must_fail(-1); /* <0 == failure */\n+\tcl_must_pass(-1); /* demonstrate a failing call */\n+}\n+\n+void test_sample__2(void)\n+{\n+\tcl_fixture_sandbox(\"test\");\n+\n+\tcl_assert(file_size(\"test/nonexistent\") == -1);\n+\tcl_assert(file_size(\"test/file\") > 0);\n+\tcl_assert(100 == 101);\n+}\n+\n+void test_sample__strings(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_s(\"expected\", actual);\n+\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n+\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n+}\n+\n+void test_sample__strings_with_length(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_strn(\"expected_\", actual, 8);\n+\tcl_assert_equal_strn(\"exactly\", actual, 2);\n+\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n+\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n+}\n+\n+void test_sample__int(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i(100, value);\n+\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n+}\n+\n+void test_sample__int_fmt(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n+}\n+\n+void test_sample__bool(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n+\tcl_assert_equal_b(0, value);\n+}\n+\n+void test_sample__ptr(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n+\tcl_assert_equal_p(&actual, actual);\n+}\n-- \n2.46.0.dirty\n\n"},{"id":"499672","messageId":"5195d084d3c1bee76e7e424afec2c09bff8f5dde.1722415748.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[RFC PATCH 2/3] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-07-31T09:04:21Z","receivedAt":"2024-07-31T09:04:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Wire up the clar unit testing framework by introducing a new\n\"unit-tests\" executable. In contrast to the existing framework, this\nwill result in a single executable for all test suites. The ability to\npick specific tests to execute is retained via functionality built into\nthe clar itself.\n\nNote that we need to be a bit careful about how we need to invalidate\nour Makefile rules. While we obviously have to regenerate the clar suite\nwhen our test suites change, we also have to invalidate it in case any\nof the test suites gets removed. We do so by using our typical pattern\nof creating a `GIT-TEST-SUITES` file that gets updated whenever the set\nof test suites changes, so that we can easily depend on that file.\n\nAnother specialty is that we generate a \"clar-decls.h\" file. The test\nfunctions are neither static, nor do they have external declarations.\nThis is because they are getting parsed via \"generate.py\", which then\ncreates the external generations that get populated into an array. These\ndeclarations are only seen by the main function though.\n\nThe consequence is that we will get a bunch of \"missing prototypes\"\nerrors from our compiler for each of these test functions. To fix those\nerrors, we extract the `extern` declarations from \"clar.suite\" and put\nthem into a standalone header that then gets included by each of our\nunit tests. This gets rid of compiler warnings for every function which\nhas been extracted by \"generate.py\". More importantly though, it does\n_not_ get rid of warnings in case a function really isn't being used by\nanything. Thus, it would cause a compiler error if a function name was\nmistyped and thus not picked up by \"generate.py\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore               |  1 +\n Makefile                 | 34 ++++++++++++++++++++++++++++++----\n t/Makefile               |  1 +\n t/unit-tests/.gitignore  |  3 +++\n t/unit-tests/unit-test.c | 16 ++++++++++++++++\n t/unit-tests/unit-test.h |  3 +++\n 6 files changed, 54 insertions(+), 4 deletions(-)\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\ndiff --git a/.gitignore b/.gitignore\nindex 8caf3700c2..6687bd6db4 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,6 +9,7 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n+/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex d6479092a0..658acb4d48 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1332,6 +1332,11 @@ THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n \n+UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n+UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n+\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n@@ -2711,6 +2716,7 @@ OBJECTS += $(XDIFF_OBJS)\n OBJECTS += $(FUZZ_OBJS)\n OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n OBJECTS += $(UNIT_TEST_OBJS)\n+OBJECTS += $(UNIT_TESTS_OBJS)\n \n ifndef NO_CURL\n \tOBJECTS += http.o http-walker.o remote-curl.o\n@@ -3213,7 +3219,7 @@ endif\n \n test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))\n \n-all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)\n+all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n \n bin-wrappers/%: wrap-for-bin.sh\n \t$(call mkdir_p_parent_template)\n@@ -3644,7 +3650,7 @@ endif\n \n artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n \t\tGIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \\\n-\t\t$(UNIT_TEST_PROGS) $(MOFILES)\n+\t\t$(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) $(MOFILES)\n \t$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \\\n \t\tSHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'\n \ttest -n \"$(ARTIFACTS_DIRECTORY)\"\n@@ -3700,6 +3706,7 @@ cocciclean:\n \n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n+\t$(RM) $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/.clarcache\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3859,7 +3866,26 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n \n+GIT-TEST-SUITES: FORCE\n+\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n+\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n+\t\techo >&2 \"    * new test suites\"; \\\n+\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n+\t\trm -f $(UNIT_TESTS_DIR)/.clarcache; \\\n+            fi\n+\n+$(UNIT_TEST_DIR)/clar.suite: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n+\t$(QUIET_GEN)$(UNIT_TEST_DIR)/clar/generate.py $(UNIT_TEST_DIR) >/dev/null\n+\t@touch $@\n+$(UNIT_TEST_DIR)/clar-decls.h: $(UNIT_TEST_DIR)/clar.suite\n+\t$(QUIET_GEN)grep '^extern void' $^ >$@\n+$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR) -I$(UNIT_TEST_DIR)/clar\n+$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n+\t$(call mkdir_p_parent_template)\n+\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n+\n .PHONY: build-unit-tests unit-tests\n-build-unit-tests: $(UNIT_TEST_PROGS)\n-unit-tests: $(UNIT_TEST_PROGS) t/helper/test-tool$X\n+build-unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n+unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) t/helper/test-tool$X\n \t$(MAKE) -C t/ unit-tests\ndiff --git a/t/Makefile b/t/Makefile\nindex 4c30e7c06f..55f740ae29 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -48,6 +48,7 @@ CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.tes\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n+UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n UNIT_TESTS = $(sort $(UNIT_TEST_PROGRAMS))\n UNIT_TESTS_NO_DIR = $(notdir $(UNIT_TESTS))\n \ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex 5e56e040ec..b8d46f7bb1 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1 +1,4 @@\n /bin\n+/.clarcache\n+/clar.suite\n+/clar-decls.h\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nnew file mode 100644\nindex 0000000000..d7eecc384c\n--- /dev/null\n+++ b/t/unit-tests/unit-test.c\n@@ -0,0 +1,16 @@\n+#include \"unit-test.h\"\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tconst char **args;\n+\tint ret;\n+\n+\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n+\tDUP_ARRAY(args, argv, argc + 1);\n+\targs[argc++] = \"-t\";\n+\n+\tret = clar_test(argc, (char **) args);\n+\n+\tfree(args);\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nnew file mode 100644\nindex 0000000000..99d59df1b0\n--- /dev/null\n+++ b/t/unit-tests/unit-test.h\n@@ -0,0 +1,3 @@\n+#include \"git-compat-util.h\"\n+#include \"clar.h\"\n+#include \"clar-decls.h\"\n-- \n2.46.0.dirty\n\n"},{"id":"499673","messageId":"faacf2a8467fbcc4c0fa0b508384e383486decb3.1722415748.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[RFC PATCH 3/3] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-07-31T09:04:25Z","receivedAt":"2024-07-31T09:04:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the strvec tests to use the new clar unit testing framework.\nThis is a first test balloon that demonstrates how the testing infra for\nclar-based tests looks like.\n\nThe tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\nrunning that binary, it generates TAP output:\n\n    # ./t/unit-tests/bin/unit-tests\n    TAP version 13\n    # start of suite 1: strvec\n    ok 1 - strvec::init\n    ok 2 - strvec::dynamic_init\n    ok 3 - strvec::clear\n    ok 4 - strvec::push\n    ok 5 - strvec::pushft_pushf\n    ok 6 - strvec::pushl\n    ok 7 - strvec::pushv\n    ok 8 - strvec::replace_at_head\n    ok 9 - strvec::replace_at_tail\n    ok 10 - strvec::replace_in_between\n    ok 11 - strvec::replace_with_substring\n    ok 12 - strvec::remove_at_head\n    ok 13 - strvec::remove_at_tail\n    ok 14 - strvec::remove_in_between\n    ok 15 - strvec::pop_empty_array\n    ok 16 - strvec::pop_non_empty_array\n    ok 17 - strvec::split_empty_string\n    ok 18 - strvec::split_single_item\n    ok 19 - strvec::split_multiple_items\n    ok 20 - strvec::split_whitespace_only\n    ok 21 - strvec::split_multiple_consecutive_whitespaces\n    ok 22 - strvec::detach\n    1..22\n\nThe binary also supports some parameters that allow us to run only a\nsubset of unit tests or alter the output:\n\n    $ ./t/unit-tests/bin/unit-tests -h\n    Usage: ./t/unit-tests/bin/unit-tests [options]\n\n    Options:\n      -sname        Run only the suite with `name` (can go to individual test name)\n      -iname        Include the suite with `name`\n      -xname        Exclude the suite with `name`\n      -v            Increase verbosity (show suite names)\n      -q            Only report tests that had an error\n      -Q            Quit as soon as a test fails\n      -t            Display results in tap format\n      -l            Print suite names\n      -r[filename]  Write summary file (to the optional filename)\n\nFurthermore, running `make unit-tests` runs the binary along with all\nthe other unit tests we have.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                              |   2 +-\n t/unit-tests/{t-strvec.c => strvec.c} | 124 ++++++++++----------------\n 2 files changed, 46 insertions(+), 80 deletions(-)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n\ndiff --git a/Makefile b/Makefile\nindex 658acb4d48..8ebcbdc95a 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1332,6 +1332,7 @@ THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n \n+UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n@@ -1348,7 +1349,6 @@ UNIT_TEST_PROGRAMS += t-reftable-basics\n UNIT_TEST_PROGRAMS += t-reftable-record\n UNIT_TEST_PROGRAMS += t-strbuf\n UNIT_TEST_PROGRAMS += t-strcmp-offset\n-UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-trailer\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/strvec.c\nsimilarity index 54%\nrename from t/unit-tests/t-strvec.c\nrename to t/unit-tests/strvec.c\nindex d4615ab06d..5edb1ae326 100644\n--- a/t/unit-tests/t-strvec.c\n+++ b/t/unit-tests/strvec.c\n@@ -1,11 +1,11 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n \n #define check_strvec(vec, ...) \\\n-\tcheck_strvec_loc(TEST_LOCATION(), vec, __VA_ARGS__)\n+\tcheck_strvec_loc(__FILE__, __func__, __LINE__, vec, __VA_ARGS__)\n LAST_ARG_MUST_BE_NULL\n-static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n+static void check_strvec_loc(const char *file, const char *func, size_t line, struct strvec *vec, ...)\n {\n \tva_list ap;\n \tsize_t nr = 0;\n@@ -16,54 +16,47 @@ static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n \t\tif (!str)\n \t\t\tbreak;\n \n-\t\tif (!check_uint(vec->nr, >, nr) ||\n-\t\t    !check_uint(vec->alloc, >, nr) ||\n-\t\t    !check_str(vec->v[nr], str)) {\n-\t\t\tstruct strbuf msg = STRBUF_INIT;\n-\t\t\tstrbuf_addf(&msg, \"strvec index %\"PRIuMAX, (uintmax_t) nr);\n-\t\t\ttest_assert(loc, msg.buf, 0);\n-\t\t\tstrbuf_release(&msg);\n-\t\t\tva_end(ap);\n-\t\t\treturn;\n-\t\t}\n+\t\tclar__assert(vec->nr > nr, file, func, line, \"Vector size too small\", NULL, 1);\n+\t\tclar__assert(vec->alloc > nr, file, func, line, \"Vector allocation too small\", NULL, 1);\n+\t\tcl_assert_equal_s(vec->v[nr], str);\n \n \t\tnr++;\n \t}\n \tva_end(ap);\n \n-\tcheck_uint(vec->nr, ==, nr);\n-\tcheck_uint(vec->alloc, >=, nr);\n-\tcheck_pointer_eq(vec->v[nr], NULL);\n+\tcl_assert(vec->nr == nr);\n+\tcl_assert(vec->alloc >= nr);\n+\tcl_assert_equal_p(vec->v[nr], NULL);\n }\n \n-static void t_static_init(void)\n+void test_strvec__init(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert(vec.nr == 0);\n+\tcl_assert(vec.alloc == 0);\n }\n \n-static void t_dynamic_init(void)\n+void test_strvec__dynamic_init(void)\n {\n \tstruct strvec vec;\n \tstrvec_init(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert(vec.nr == 0);\n+\tcl_assert(vec.alloc == 0);\n }\n \n-static void t_clear(void)\n+void test_strvec__clear(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_push(&vec, \"foo\");\n \tstrvec_clear(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert(vec.nr == 0);\n+\tcl_assert(vec.alloc == 0);\n }\n \n-static void t_push(void)\n+void test_strvec__push(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \n@@ -76,7 +69,7 @@ static void t_push(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushf(void)\n+void test_strvec__pushft_pushf(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushf(&vec, \"foo: %d\", 1);\n@@ -84,7 +77,7 @@ static void t_pushf(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushl(void)\n+void test_strvec__pushl(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -92,7 +85,7 @@ static void t_pushl(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushv(void)\n+void test_strvec__pushv(void)\n {\n \tconst char *strings[] = {\n \t\t\"foo\", \"bar\", \"baz\", NULL,\n@@ -105,7 +98,7 @@ static void t_pushv(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_head(void)\n+void test_strvec__replace_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -114,7 +107,7 @@ static void t_replace_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_tail(void)\n+void test_strvec__replace_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -123,7 +116,7 @@ static void t_replace_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_in_between(void)\n+void test_strvec__replace_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -132,7 +125,7 @@ static void t_replace_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_with_substring(void)\n+void test_strvec__replace_with_substring(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", NULL);\n@@ -141,7 +134,7 @@ static void t_replace_with_substring(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_head(void)\n+void test_strvec__remove_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -150,7 +143,7 @@ static void t_remove_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_tail(void)\n+void test_strvec__remove_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -159,7 +152,7 @@ static void t_remove_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_in_between(void)\n+void test_strvec__remove_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -168,7 +161,7 @@ static void t_remove_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_empty_array(void)\n+void test_strvec__pop_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pop(&vec);\n@@ -176,7 +169,7 @@ static void t_pop_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_non_empty_array(void)\n+void test_strvec__pop_non_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -185,7 +178,7 @@ static void t_pop_non_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_empty_string(void)\n+void test_strvec__split_empty_string(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"\");\n@@ -193,7 +186,7 @@ static void t_split_empty_string(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_single_item(void)\n+void test_strvec__split_single_item(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\");\n@@ -201,7 +194,7 @@ static void t_split_single_item(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_items(void)\n+void test_strvec__split_multiple_items(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo bar baz\");\n@@ -209,7 +202,7 @@ static void t_split_multiple_items(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_whitespace_only(void)\n+void test_strvec__split_whitespace_only(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \" \\t\\n\");\n@@ -217,7 +210,7 @@ static void t_split_whitespace_only(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_consecutive_whitespaces(void)\n+void test_strvec__split_multiple_consecutive_whitespaces(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\\n\\t bar\");\n@@ -225,7 +218,7 @@ static void t_split_multiple_consecutive_whitespaces(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_detach(void)\n+void test_strvec__detach(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tconst char **detached;\n@@ -233,40 +226,13 @@ static void t_detach(void)\n \tstrvec_push(&vec, \"foo\");\n \n \tdetached = strvec_detach(&vec);\n-\tcheck_str(detached[0], \"foo\");\n-\tcheck_pointer_eq(detached[1], NULL);\n+\tcl_assert_equal_s(detached[0], \"foo\");\n+\tcl_assert_equal_p(detached[1], NULL);\n \n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert(vec.nr == 0);\n+\tcl_assert(vec.alloc == 0);\n \n \tfree((char *) detached[0]);\n \tfree(detached);\n }\n-\n-int cmd_main(int argc, const char **argv)\n-{\n-\tTEST(t_static_init(), \"static initialization\");\n-\tTEST(t_dynamic_init(), \"dynamic initialization\");\n-\tTEST(t_clear(), \"clear\");\n-\tTEST(t_push(), \"push\");\n-\tTEST(t_pushf(), \"pushf\");\n-\tTEST(t_pushl(), \"pushl\");\n-\tTEST(t_pushv(), \"pushv\");\n-\tTEST(t_replace_at_head(), \"replace at head\");\n-\tTEST(t_replace_in_between(), \"replace in between\");\n-\tTEST(t_replace_at_tail(), \"replace at tail\");\n-\tTEST(t_replace_with_substring(), \"replace with substring\");\n-\tTEST(t_remove_at_head(), \"remove at head\");\n-\tTEST(t_remove_in_between(), \"remove in between\");\n-\tTEST(t_remove_at_tail(), \"remove at tail\");\n-\tTEST(t_pop_empty_array(), \"pop with empty array\");\n-\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n-\tTEST(t_split_empty_string(), \"split empty string\");\n-\tTEST(t_split_single_item(), \"split single item\");\n-\tTEST(t_split_multiple_items(), \"split multiple items\");\n-\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n-\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n-\tTEST(t_detach(), \"detach\");\n-\treturn test_done();\n-}\n-- \n2.46.0.dirty\n\n"},{"id":"499707","messageId":"xmqq7cd18srf.fsf@gitster.g","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"Re: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-07-31T15:51:00Z","receivedAt":"2024-07-31T15:51:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>   - The clar gives us the ability to pick which tests to run via command\n>     line parameters, which I personally like more than picking the\n>     specific binary to run.\n\nOne thing I am very unhappy about the current t/unit-tests/ is that\nthe GIT_SKIP_TESTS mechanism is not effective at all.  If we can\nwrap clar's test selection syntax inside t/Makefile to work with\nGIT_SKIP_TESTS (or its superset equivalent), that would be a great\nplus.\n\n>   - The clar replaces some test assertions that we already have. They\n>     feel a bit more mature, but overall there aren't all that many\n>     assertions available. If we wanted to pick it up, then we'd likely\n>     have to add some more wrappers.\n\nThat is a slight bummer, as importing an externally developed one is\nwith the hope that we won't have to enhance or maintain it, but\nwe'll see how much burden it will be.\n\n>   - The clar uses longjmp instead of manually having to `return` from\n>     functions in case there was an assertion failure. This is easier to\n>     work with in my opinion.\n>\n> Also, note that I only tested this on my Linux machine. I have no clue\n> whether this works as-is on Windows, but I do know that libgit2 tests\n> run on Linux, macOS and Windows. So it should work in theory, it's just\n> a matter of polishing this series.\n>\n> I'm happy to hear your thoughts on this, even if it ultimately ends up\n> being shot down.\n\nThanks for getting the ball going.  Let's see how fast and far it rolls.\n"},{"id":"499708","messageId":"008901dae362$386ae280$a940a780$@nexbridge.com","threadId":"61874","inReplyTo":"xmqq7cd18srf.fsf@gitster.g","subject":"RE: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-07-31T15:56:29Z","receivedAt":"2024-07-31T15:56:46Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Wednesday, July 31, 2024 11:51 AM, Junio C Hamano wrote:\n>Patrick Steinhardt <ps@pks.im> writes:\n>\n>>   - The clar gives us the ability to pick which tests to run via command\n>>     line parameters, which I personally like more than picking the\n>>     specific binary to run.\n>\n>One thing I am very unhappy about the current t/unit-tests/ is that the\n>GIT_SKIP_TESTS mechanism is not effective at all.  If we can wrap clar's test selection\n>syntax inside t/Makefile to work with GIT_SKIP_TESTS (or its superset equivalent),\n>that would be a great plus.\n>\n>>   - The clar replaces some test assertions that we already have. They\n>>     feel a bit more mature, but overall there aren't all that many\n>>     assertions available. If we wanted to pick it up, then we'd likely\n>>     have to add some more wrappers.\n>\n>That is a slight bummer, as importing an externally developed one is with the hope\n>that we won't have to enhance or maintain it, but we'll see how much burden it will\n>be.\n>\n>>   - The clar uses longjmp instead of manually having to `return` from\n>>     functions in case there was an assertion failure. This is easier to\n>>     work with in my opinion.\n>>\n>> Also, note that I only tested this on my Linux machine. I have no clue\n>> whether this works as-is on Windows, but I do know that libgit2 tests\n>> run on Linux, macOS and Windows. So it should work in theory, it's\n>> just a matter of polishing this series.\n>>\n>> I'm happy to hear your thoughts on this, even if it ultimately ends up\n>> being shot down.\n>\n>Thanks for getting the ball going.  Let's see how fast and far it rolls.\n\nI'm sorry for being so behind the curve... what is clar and where does it run?\n\n"},{"id":"499723","messageId":"cf23d831-9412-408a-8e68-170a8a476808@web.de","threadId":"61874","inReplyTo":"5195d084d3c1bee76e7e424afec2c09bff8f5dde.1722415748.git.ps@pks.im","subject":"Re: [RFC PATCH 2/3] Makefile: wire up the clar unit testing framework","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2024-07-31T16:48:36Z","receivedAt":"2024-07-31T16:49:17Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 31.07.24 um 11:04 schrieb Patrick Steinhardt:\n> Wire up the clar unit testing framework by introducing a new\n> \"unit-tests\" executable. In contrast to the existing framework, this\n> will result in a single executable for all test suites. The ability to\n> pick specific tests to execute is retained via functionality built into\n> the clar itself.\n>\n> Note that we need to be a bit careful about how we need to invalidate\n> our Makefile rules. While we obviously have to regenerate the clar suite\n> when our test suites change, we also have to invalidate it in case any\n> of the test suites gets removed. We do so by using our typical pattern\n> of creating a `GIT-TEST-SUITES` file that gets updated whenever the set\n> of test suites changes, so that we can easily depend on that file.\n>\n> Another specialty is that we generate a \"clar-decls.h\" file. The test\n> functions are neither static, nor do they have external declarations.\n> This is because they are getting parsed via \"generate.py\", which then\n> creates the external generations that get populated into an array. These\n> declarations are only seen by the main function though.\n>\n> The consequence is that we will get a bunch of \"missing prototypes\"\n> errors from our compiler for each of these test functions. To fix those\n> errors, we extract the `extern` declarations from \"clar.suite\" and put\n> them into a standalone header that then gets included by each of our\n> unit tests. This gets rid of compiler warnings for every function which\n> has been extracted by \"generate.py\". More importantly though, it does\n> _not_ get rid of warnings in case a function really isn't being used by\n> anything. Thus, it would cause a compiler error if a function name was\n> mistyped and thus not picked up by \"generate.py\".\n>\n\n> +$(UNIT_TEST_DIR)/clar.suite: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n> +\t$(QUIET_GEN)$(UNIT_TEST_DIR)/clar/generate.py $(UNIT_TEST_DIR) >/dev/null\n\nThis uses the Python interpreter from the shebang line in generate.py,\nwhich is python.  On my system I only have python3 and python3.12, but\nnot python.  Easily fixed, of course, but a way to configure the\ninterpreter name would be nice.\n\nThis gave me extra motivation to come up with the clunky patch below\nto replace Python with sed and awk.  That and your statement that clar\ndoesn't have to be perfect in the other thread. ;)\n\nIt reverses the order of dependencies (builds clar-decls.h first), not\nsure if that has downsides.  And the sed pattern is simpler than the\none in generate.py, just out of laziness.\n\nRené\n\n---\n .gitignore                |  1 -\n Makefile                  | 20 ++++++-----------\n t/unit-tests/.gitignore   |  1 -\n t/unit-tests/generate.awk | 47 +++++++++++++++++++++++++++++++++++++++\n 4 files changed, 54 insertions(+), 15 deletions(-)\n create mode 100644 t/unit-tests/generate.awk\n\ndiff --git a/.gitignore b/.gitignore\nindex 6687bd6db4..8caf3700c2 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,7 +9,6 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n-/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex 8ebcbdc95a..1ffde38de5 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3706,7 +3706,7 @@ cocciclean:\n\n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n-\t$(RM) $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/.clarcache\n+\t$(RM) $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/clar-decls.h\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3866,19 +3866,13 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n\n-GIT-TEST-SUITES: FORCE\n-\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n-\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n-\t\techo >&2 \"    * new test suites\"; \\\n-\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n-\t\trm -f $(UNIT_TESTS_DIR)/.clarcache; \\\n-            fi\n+$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h $(UNIT_TEST_DIR)/generate.awk\n+\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/generate.awk $(UNIT_TEST_DIR)/clar-decls.h >$@\n+$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES))\n+\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n+\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n+\tdone >$@\n\n-$(UNIT_TEST_DIR)/clar.suite: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n-\t$(QUIET_GEN)$(UNIT_TEST_DIR)/clar/generate.py $(UNIT_TEST_DIR) >/dev/null\n-\t@touch $@\n-$(UNIT_TEST_DIR)/clar-decls.h: $(UNIT_TEST_DIR)/clar.suite\n-\t$(QUIET_GEN)grep '^extern void' $^ >$@\n $(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n $(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR) -I$(UNIT_TEST_DIR)/clar\n $(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex b8d46f7bb1..d0632ec7f9 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1,4 +1,3 @@\n /bin\n-/.clarcache\n /clar.suite\n /clar-decls.h\ndiff --git a/t/unit-tests/generate.awk b/t/unit-tests/generate.awk\nnew file mode 100644\nindex 0000000000..3c78253866\n--- /dev/null\n+++ b/t/unit-tests/generate.awk\n@@ -0,0 +1,47 @@\n+function add_suite(suite, initialize, cleanup, count) {\n+\tif (!suite) return\n+\tsuite_count++\n+\tcallback_count += count\n+\tsuites = suites \"    {\\n\"\n+\tsuites = suites \"        \\\"\" suite \"\\\",\\n\"\n+\tsuites = suites \"        \" initialize \",\\n\"\n+\tsuites = suites \"        \" cleanup \",\\n\"\n+\tsuites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n+\tsuites = suites \"    },\\n\"\n+}\n+BEGIN {\n+\tsuites = \"static struct clar_suite _clar_suites[] = {\\n\"\n+}\n+{\n+\tprint\n+\tname = $3; sub(/\\(.*$/, \"\", name)\n+\tsuite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n+\tshort_name = name; sub(/^.*__/, \"\", short_name)\n+\tcb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n+\tif (suite != prev_suite) {\n+\t\tadd_suite(prev_suite, initialize, cleanup, count)\n+\t\tif (callbacks) callbacks = callbacks \"};\\n\"\n+\t\tcallbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n+\t\tinitialize = \"{ NULL, NULL }\"\n+\t\tcleanup = \"{ NULL, NULL }\"\n+\t\tcount = 0\n+\t\tprev_suite = suite\n+\t}\n+\tif (short_name == \"initialize\") {\n+\t\tinitialize = cb\n+\t} else if (short_name == \"cleanup\") {\n+\t\tcleanup = cb\n+\t} else {\n+\t\tcallbacks = callbacks \"    \" cb \",\\n\"\n+\t\tcount++\n+\t}\n+}\n+END {\n+\tadd_suite(suite, initialize, cleanup, count)\n+\tsuites = suites \"};\"\n+\tif (callbacks) callbacks = callbacks \"};\"\n+\tprint callbacks\n+\tprint suites\n+\tprint \"static const size_t _clar_suite_count = \" suite_count \";\"\n+\tprint \"static const size_t _clar_callback_count = \" callback_count \";\"\n+}\n--\n2.46.0\n"},{"id":"499724","messageId":"xmqqfrrp7bbu.fsf@gitster.g","threadId":"61874","inReplyTo":"008901dae362$386ae280$a940a780$@nexbridge.com","subject":"Re: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-07-31T16:52:53Z","receivedAt":"2024-07-31T16:52:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"<rsbecker@nexbridge.com> writes:\n\n> I'm sorry for being so behind the curve... what is clar and where does it run?\n\nWe have t/unit-test/test-lib.[ch] that are our home-grown unit test\nframework.  A handful of tests have been written to use it, when you\nsay \"make test\", or \"(cd t && make)\", unit tests binaries linked\nwith the home-grown unit test framework run.\n\nclar is a _potential_ replacement for our home-grown framework,\nsuggested here because it would be nicer if we can use off-the-shelf\ncomponent instead of having to enhance and maintain our own.\n\nWhere and how it runs does not change even after clar turns out to\nbe good enough for our purpose and we commit to replace our\nhome-grown unit test framework with it.\n\n"},{"id":"499726","messageId":"xmqqbk2d7ax9.fsf@gitster.g","threadId":"61874","inReplyTo":"5195d084d3c1bee76e7e424afec2c09bff8f5dde.1722415748.git.ps@pks.im","subject":"Re: [RFC PATCH 2/3] Makefile: wire up the clar unit testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-07-31T17:01:38Z","receivedAt":"2024-07-31T17:01:46Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A trivial fix-up to be squashed into this step.\n\n Makefile | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git c/Makefile w/Makefile\nindex 8ebcbdc95a..d561789582 100644\n--- c/Makefile\n+++ w/Makefile\n@@ -3735,6 +3735,7 @@ ifndef NO_TCLTK\n \t$(MAKE) -C git-gui clean\n endif\n \t$(RM) GIT-VERSION-FILE GIT-CFLAGS GIT-LDFLAGS GIT-BUILD-OPTIONS\n+\t$(RM) GIT-TEST-SUITES $(UNIT_TEST_DIR)/clar-decls.h\n \t$(RM) GIT-USER-AGENT GIT-PREFIX\n \t$(RM) GIT-SCRIPT-DEFINES GIT-PERL-DEFINES GIT-PERL-HEADER GIT-PYTHON-VARS\n ifdef MSVC\n"},{"id":"499732","messageId":"463oz7j4btei57brq42zlnsydguu74n2n22rhdq7iwjk4y2bei@6r32nghurrmr","threadId":"61874","inReplyTo":"b3db953e88ece99b441993c9248223cc0ecc6be8.1722415748.git.ps@pks.im","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2024-07-31T18:27:13Z","receivedAt":"2024-07-31T18:27:22Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2024.07.31 11:04, Patrick Steinhardt wrote:\n> Import the clar unit testing framework at commit faa8419 (Merge pull\n> request #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\n> will be wired up in subsequent commits.\n\nRather than forking our own copy of clar, could we just add it as a\nsubmodule instead?\n"},{"id":"499733","messageId":"avbtyqklmtwmz63vbpevkz4xyv3dboyghwhkodxjlb3ur6zt36@afi5ptrsvpz2","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"Re: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2024-07-31T18:33:55Z","receivedAt":"2024-07-31T18:34:01Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2024.07.31 11:04, Patrick Steinhardt wrote:\n> Hi,\n> \n> there's been some discussion around extending our unit testing framework\n> to avoid duplication when declaring test functions. Right now, a testing\n> function has to be declared and then wired up via the test's main\n> function, which can be a bit annoying. In the thread, René proposes an\n> alternative that gets rid of this duplication by using macros. And while\n> that does solve the issue, there were some concerns about things being\n> too much \"magic\" while at the same time not being flexible enough.\n> \n> Part of the discussion revolved around whether we maybe want to have a\n> proper unit testing framework in our codebase instead of reinventing the\n> wheel. As I quite liked the \"clar\" [2] testing framework from back when\n> I was still developing libgit2 I proposed it as a possible alternative.\n> This patch series wires up the clar framework as a proof of concept and\n> converts the strvec test suite to use it.\n> \n> The magic to avoid the above code duplication is quite self-contained in\n> a \"generate.py\" file. This script extracts function declarations from\n> all unit test suites and then writes those into a \"clar.suite\" header\n> file. All that one needs to do is thus to declare a function with a\n> specific name \"test_<suite>__<name>\" and then everything else gets wired\n> up automatically.\n> \n> Whether this is better than the solution proposed by René is probably a\n> matter of taste. While I'm not a huge fan of the macro-based solution,\n> I don't want to veto it either (not that I'd have that power anyway). So\n> please, you should rather read this as a proof of concept to see how\n> alternatives could look like such that we have a better picture of where\n> we want to end up.\n> \n> Some random thoughts:\n> \n>   - The mandated Python dependency is suboptimal in my opinion.\n>     Rewriting the script in e.g. Perl should be easy enough though, it's\n>     no rocket science.\n> \n>   - I prefer that the proposed solution results in a single binary as\n>     compared to one binary per test system.\n\nDoes clar allow running test functions in parallel? With multiple\nbinaries, we can at least run independent tests in parallel (although\nright now the unit tests are fewer and so much faster than the shell\ntests that it's hardly noticeable).\n\n>   - The clar gives us the ability to pick which tests to run via command\n>     line parameters, which I personally like more than picking the\n>     specific binary to run.\n\nYes this is a nice improvement.\n\n>   - The clar replaces some test assertions that we already have. They\n>     feel a bit more mature, but overall there aren't all that many\n>     assertions available. If we wanted to pick it up, then we'd likely\n>     have to add some more wrappers.\n> \n>   - The clar uses longjmp instead of manually having to `return` from\n>     functions in case there was an assertion failure. This is easier to\n>     work with in my opinion.\n> \n> Also, note that I only tested this on my Linux machine. I have no clue\n> whether this works as-is on Windows, but I do know that libgit2 tests\n> run on Linux, macOS and Windows. So it should work in theory, it's just\n> a matter of polishing this series.\n> \n> I'm happy to hear your thoughts on this, even if it ultimately ends up\n> being shot down.\n\nAs part of the original unit-test series, I wrote a comparison between\ndifferent frameworks: Documentation/technical/unit-tests.txt, poorly\nrendered at [1]. Could you add a row to the table evaluating clar on the\nindividual points there?\n\n[1] https://git-scm.com/docs/unit-tests#framework-selection\n\n> Patrick\n> \n> [1]: <85b6b8a9-ee5f-42ab-bcbc-49976b30ef33@web.de>\n> [2]: https://github.com/clar-test/clar\n> \n> Patrick Steinhardt (3):\n>   t: import the clar unit testing framework\n>   Makefile: wire up the clar unit testing framework\n>   t/unit-tests: convert strvec tests to use clar\n> \n>  .gitignore                                 |   1 +\n>  Makefile                                   |  36 +-\n>  t/Makefile                                 |   1 +\n>  t/unit-tests/.gitignore                    |   3 +\n>  t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n>  t/unit-tests/clar/COPYING                  |  15 +\n>  t/unit-tests/clar/README.md                | 329 ++++++++\n>  t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n>  t/unit-tests/clar/clar.h                   | 173 +++++\n>  t/unit-tests/clar/clar/fixtures.h          |  50 ++\n>  t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n>  t/unit-tests/clar/clar/print.h             | 211 ++++++\n>  t/unit-tests/clar/clar/sandbox.h           | 154 ++++\n>  t/unit-tests/clar/clar/summary.h           | 143 ++++\n>  t/unit-tests/clar/generate.py              | 267 +++++++\n>  t/unit-tests/clar/test/.gitignore          |   5 +\n>  t/unit-tests/clar/test/Makefile            |  39 +\n>  t/unit-tests/clar/test/clar_test.h         |  16 +\n>  t/unit-tests/clar/test/main.c              |  40 +\n>  t/unit-tests/clar/test/main.c.sample       |  27 +\n>  t/unit-tests/clar/test/resources/test/file |   1 +\n>  t/unit-tests/clar/test/sample.c            |  84 ++\n>  t/unit-tests/{t-strvec.c => strvec.c}      | 124 ++-\n>  t/unit-tests/unit-test.c                   |  16 +\n>  t/unit-tests/unit-test.h                   |   3 +\n>  25 files changed, 3041 insertions(+), 84 deletions(-)\n>  create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n>  create mode 100644 t/unit-tests/clar/COPYING\n>  create mode 100644 t/unit-tests/clar/README.md\n>  create mode 100644 t/unit-tests/clar/clar.c\n>  create mode 100644 t/unit-tests/clar/clar.h\n>  create mode 100644 t/unit-tests/clar/clar/fixtures.h\n>  create mode 100644 t/unit-tests/clar/clar/fs.h\n>  create mode 100644 t/unit-tests/clar/clar/print.h\n>  create mode 100644 t/unit-tests/clar/clar/sandbox.h\n>  create mode 100644 t/unit-tests/clar/clar/summary.h\n>  create mode 100755 t/unit-tests/clar/generate.py\n>  create mode 100644 t/unit-tests/clar/test/.gitignore\n>  create mode 100644 t/unit-tests/clar/test/Makefile\n>  create mode 100644 t/unit-tests/clar/test/clar_test.h\n>  create mode 100644 t/unit-tests/clar/test/main.c\n>  create mode 100644 t/unit-tests/clar/test/main.c.sample\n>  create mode 100644 t/unit-tests/clar/test/resources/test/file\n>  create mode 100644 t/unit-tests/clar/test/sample.c\n>  rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n>  create mode 100644 t/unit-tests/unit-test.c\n>  create mode 100644 t/unit-tests/unit-test.h\n> \n> -- \n> 2.46.0.dirty\n> \n\n\n"},{"id":"499737","messageId":"xmqqv80l1hgs.fsf@gitster.g","threadId":"61874","inReplyTo":"463oz7j4btei57brq42zlnsydguu74n2n22rhdq7iwjk4y2bei@6r32nghurrmr","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-07-31T19:36:51Z","receivedAt":"2024-07-31T19:36:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n> On 2024.07.31 11:04, Patrick Steinhardt wrote:\n>> Import the clar unit testing framework at commit faa8419 (Merge pull\n>> request #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\n>> will be wired up in subsequent commits.\n>\n> Rather than forking our own copy of clar, could we just add it as a\n> submodule instead?\n\nI doubt that forking was Patrick's intention to begin with.\nConvenience is.\n\nIt certainly is a possibility to bind it as a submodule, but it\nwould be easier for everybody to futz with and experiment, at least\nwhile it is in its RFC phase.\n\nThanks.\n"},{"id":"499738","messageId":"00a801dae384$de2780d0$9a768270$@nexbridge.com","threadId":"61874","inReplyTo":"463oz7j4btei57brq42zlnsydguu74n2n22rhdq7iwjk4y2bei@6r32nghurrmr","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-07-31T20:04:29Z","receivedAt":"2024-07-31T20:04:47Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Wednesday, July 31, 2024 2:27 PM, Josh Steadmon wrote:\n>On 2024.07.31 11:04, Patrick Steinhardt wrote:\n>> Import the clar unit testing framework at commit faa8419 (Merge pull\n>> request #93 from clar-test/ethomson/fixtures, 2023-12-14). The\n>> framework will be wired up in subsequent commits.\n>\n>Rather than forking our own copy of clar, could we just add it as a\nsubmodule\n>instead?\n\nWhat are the requirements to build/use this?\n\n"},{"id":"499739","messageId":"00a901dae387$c68f6f70$53ae4e50$@nexbridge.com","threadId":"61874","inReplyTo":"xmqqfrrp7bbu.fsf@gitster.g","subject":"RE: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-07-31T20:25:18Z","receivedAt":"2024-07-31T20:25:34Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Wednesday, July 31, 2024 12:53 PM, Junio C Hamano wrote:\n><rsbecker@nexbridge.com> writes:\n>\n>> I'm sorry for being so behind the curve... what is clar and where does it run?\n>\n>We have t/unit-test/test-lib.[ch] that are our home-grown unit test framework.  A\n>handful of tests have been written to use it, when you say \"make test\", or \"(cd t &&\n>make)\", unit tests binaries linked with the home-grown unit test framework run.\n>\n>clar is a _potential_ replacement for our home-grown framework, suggested here\n>because it would be nicer if we can use off-the-shelf component instead of having\n>to enhance and maintain our own.\n>\n>Where and how it runs does not change even after clar turns out to be good enough\n>for our purpose and we commit to replace our home-grown unit test framework\n>with it.\n\nWell... I would like to be able to see whether this can be built/used on NonStop just\nso I can stay ahead of the curve or be far enough in advance of it to request any\nrequired fixes to make it work on platform.\n\n\n"},{"id":"499740","messageId":"00ba01dae389$7ecb2c40$7c6184c0$@nexbridge.com","threadId":"61874","inReplyTo":"00a901dae387$c68f6f70$53ae4e50$@nexbridge.com","subject":"RE: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-07-31T20:37:37Z","receivedAt":"2024-07-31T20:37:51Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Wednesday, July 31, 2024 4:25 PM, I wrote:\n>On Wednesday, July 31, 2024 12:53 PM, Junio C Hamano wrote:\n>><rsbecker@nexbridge.com> writes:\n>>\n>>> I'm sorry for being so behind the curve... what is clar and where does it run?\n>>\n>>We have t/unit-test/test-lib.[ch] that are our home-grown unit test\n>>framework.  A handful of tests have been written to use it, when you\n>>say \"make test\", or \"(cd t && make)\", unit tests binaries linked with the home-\n>grown unit test framework run.\n>>\n>>clar is a _potential_ replacement for our home-grown framework,\n>>suggested here because it would be nicer if we can use off-the-shelf\n>>component instead of having to enhance and maintain our own.\n>>\n>>Where and how it runs does not change even after clar turns out to be\n>>good enough for our purpose and we commit to replace our home-grown\n>>unit test framework with it.\n>\n>Well... I would like to be able to see whether this can be built/used on NonStop just\n>so I can stay ahead of the curve or be far enough in advance of it to request any\n>required fixes to make it work on platform.\n\nAfter checking out clar, I would say it has potential, but needs to be slightly more\nportable to depend on more than gcc CFLAGS (specifically -Wall). I will ask that\nof that team. It does look like it might be possible, but it should integrate with the\nconfig.mak.uname settings, so c99 or c11 (or soon c17) could be used with other\nCFLAGS from that file so maintainers only have to worry about one location.\n\n--Randall\n\n"},{"id":"499746","messageId":"xmqqmslx1bsu.fsf@gitster.g","threadId":"61874","inReplyTo":"xmqqbk2d7ax9.fsf@gitster.g","subject":"Re: [RFC PATCH 2/3] Makefile: wire up the clar unit testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-07-31T21:39:13Z","receivedAt":"2024-07-31T21:39:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> A trivial fix-up to be squashed into this step.\n\nI won't be able to address this today, but \"make sparse\" and other\nauxiliary targets also seem to break, due to their lack of\ndependence on the generated clar.suite file.\n\n\n"},{"id":"499780","messageId":"ZqtV_G3ZNNFR5dDf@tanuki","threadId":"61874","inReplyTo":"avbtyqklmtwmz63vbpevkz4xyv3dboyghwhkodxjlb3ur6zt36@afi5ptrsvpz2","subject":"Re: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T09:31:40Z","receivedAt":"2024-08-01T09:31:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 31, 2024 at 11:33:55AM -0700, Josh Steadmon wrote:\n> On 2024.07.31 11:04, Patrick Steinhardt wrote:\n> >   - I prefer that the proposed solution results in a single binary as\n> >     compared to one binary per test system.\n> \n> Does clar allow running test functions in parallel? With multiple\n> binaries, we can at least run independent tests in parallel (although\n> right now the unit tests are fewer and so much faster than the shell\n> tests that it's hardly noticeable).\n\nAh, that's something I didn't think of. clar does not support running\ntests in parallel.\n\nAs you say, I guess for now that is fine and I'd claim that it likely is\nfaster to just run all tests sequentially with a single binary if you\nalso include build times. If that claim isn't true, or if we eventually\ngrow a huge body of tests, then we should likely revert to having\nseparate binaries.\n\n> As part of the original unit-test series, I wrote a comparison between\n> different frameworks: Documentation/technical/unit-tests.txt, poorly\n> rendered at [1]. Could you add a row to the table evaluating clar on the\n> individual points there?\n> \n> [1] https://git-scm.com/docs/unit-tests#framework-selection\n\nAh, I wasn't aware of this document. I can update it depending on how\nthe discussion goes overall :)\n\nPatrick\n"},{"id":"499781","messageId":"ZqtWAvbX9jwYBnWH@tanuki","threadId":"61874","inReplyTo":"xmqq7cd18srf.fsf@gitster.g","subject":"Re: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T09:31:46Z","receivedAt":"2024-08-01T09:31:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 31, 2024 at 08:51:00AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> >   - The clar gives us the ability to pick which tests to run via command\n> >     line parameters, which I personally like more than picking the\n> >     specific binary to run.\n> \n> One thing I am very unhappy about the current t/unit-tests/ is that\n> the GIT_SKIP_TESTS mechanism is not effective at all.  If we can\n> wrap clar's test selection syntax inside t/Makefile to work with\n> GIT_SKIP_TESTS (or its superset equivalent), that would be a great\n> plus.\n\nYeah, I guess that shouldn't be too hard.\n\n> >   - The clar replaces some test assertions that we already have. They\n> >     feel a bit more mature, but overall there aren't all that many\n> >     assertions available. If we wanted to pick it up, then we'd likely\n> >     have to add some more wrappers.\n> \n> That is a slight bummer, as importing an externally developed one is\n> with the hope that we won't have to enhance or maintain it, but\n> we'll see how much burden it will be.\n\nThe clar exposes generic helpers like `cl_assert`, but also comparison\nfunctions like `cl_assert_equal_$t` for pointers, integers and strings.\nIf anything is missing, it also exposes the building blocks to add\nproject-specific assertions so that it would be easy to add for example\n`cl_assert_equal_oid`.\n\nOne thing I have been missing is non-equality comparisons like\n`cl_assert_lt_i`/`cl_assert_gt_i`. But adding that to the clar itself\nand upstreaming it should be easy enough, also because we know the\npeople maintaining it.\n\nOther than that I think it's mostly fine and should serve as a good\nbaseline.\n\nPatrick\n"},{"id":"499782","messageId":"ZqtWCHbnieW6nQKk@tanuki","threadId":"61874","inReplyTo":"00ba01dae389$7ecb2c40$7c6184c0$@nexbridge.com","subject":"Re: [RFC PATCH 0/3] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T09:31:52Z","receivedAt":"2024-08-01T09:31:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 31, 2024 at 04:37:37PM -0400, rsbecker@nexbridge.com wrote:\n> On Wednesday, July 31, 2024 4:25 PM, I wrote:\n> >On Wednesday, July 31, 2024 12:53 PM, Junio C Hamano wrote:\n> >><rsbecker@nexbridge.com> writes:\n> >>\n> >>> I'm sorry for being so behind the curve... what is clar and where does it run?\n> >>\n> >>We have t/unit-test/test-lib.[ch] that are our home-grown unit test\n> >>framework.  A handful of tests have been written to use it, when you\n> >>say \"make test\", or \"(cd t && make)\", unit tests binaries linked with the home-\n> >grown unit test framework run.\n> >>\n> >>clar is a _potential_ replacement for our home-grown framework,\n> >>suggested here because it would be nicer if we can use off-the-shelf\n> >>component instead of having to enhance and maintain our own.\n> >>\n> >>Where and how it runs does not change even after clar turns out to be\n> >>good enough for our purpose and we commit to replace our home-grown\n> >>unit test framework with it.\n> >\n> >Well... I would like to be able to see whether this can be built/used on NonStop just\n> >so I can stay ahead of the curve or be far enough in advance of it to request any\n> >required fixes to make it work on platform.\n> \n> After checking out clar, I would say it has potential, but needs to be slightly more\n> portable to depend on more than gcc CFLAGS (specifically -Wall). I will ask that\n> of that team. It does look like it might be possible, but it should integrate with the\n> config.mak.uname settings, so c99 or c11 (or soon c17) could be used with other\n> CFLAGS from that file so maintainers only have to worry about one location.\n\nI'm not sure I follow what you're saying. We don't use the Makefile of\nclar at all but integrate it into our own build system, so honoring\nconfig.mak.uname should come for free, I think.\n\nIn any case, if there are C constructs in clar that do not work right\nnow, then I'd be happy to address those issues. The clar is overall\nquite small, so it shouldn't be all that involved.\n\nPatrick\n"},{"id":"499783","messageId":"ZqtWDtqkXzjz2A8u@tanuki","threadId":"61874","inReplyTo":"00a801dae384$de2780d0$9a768270$@nexbridge.com","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T09:31:58Z","receivedAt":"2024-08-01T09:32:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 31, 2024 at 04:04:29PM -0400, rsbecker@nexbridge.com wrote:\n> On Wednesday, July 31, 2024 2:27 PM, Josh Steadmon wrote:\n> >On 2024.07.31 11:04, Patrick Steinhardt wrote:\n> >> Import the clar unit testing framework at commit faa8419 (Merge pull\n> >> request #93 from clar-test/ethomson/fixtures, 2023-12-14). The\n> >> framework will be wired up in subsequent commits.\n> >\n> >Rather than forking our own copy of clar, could we just add it as a\n> submodule\n> >instead?\n> \n> What are the requirements to build/use this?\n\nIn its current form, Python is a dependency due to \"generate.py\". I\nmentioned elsewhere though that I'd be happy to port it to our language\nof choice.\n\nOther than that it should only require a C89 compiler.\n\nPatrick\n"},{"id":"499784","messageId":"ZqtWFNSkzTBL0qrx@tanuki","threadId":"61874","inReplyTo":"xmqqv80l1hgs.fsf@gitster.g","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T09:32:04Z","receivedAt":"2024-08-01T09:32:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 31, 2024 at 12:36:51PM -0700, Junio C Hamano wrote:\n> Josh Steadmon <steadmon@google.com> writes:\n> \n> > On 2024.07.31 11:04, Patrick Steinhardt wrote:\n> >> Import the clar unit testing framework at commit faa8419 (Merge pull\n> >> request #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\n> >> will be wired up in subsequent commits.\n> >\n> > Rather than forking our own copy of clar, could we just add it as a\n> > submodule instead?\n> \n> I doubt that forking was Patrick's intention to begin with.\n> Convenience is.\n\nIndeed. If we decide to adopt it I'd like to carry any required changes\nupstream.\n\n> It certainly is a possibility to bind it as a submodule, but it\n> would be easier for everybody to futz with and experiment, at least\n> while it is in its RFC phase.\n\nI'm not too fond of using submodules, but I'm also not strictly against\nusing them. I'll do whatever works best for folks here and adapt\naccordingly.\n\nPatrick\n"},{"id":"499785","messageId":"ZqtWGrsKLIRneB5b@tanuki","threadId":"61874","inReplyTo":"cf23d831-9412-408a-8e68-170a8a476808@web.de","subject":"Re: [RFC PATCH 2/3] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T09:32:10Z","receivedAt":"2024-08-01T09:32:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 31, 2024 at 06:48:36PM +0200, René Scharfe wrote:\n> Am 31.07.24 um 11:04 schrieb Patrick Steinhardt:\n> > Wire up the clar unit testing framework by introducing a new\n> > \"unit-tests\" executable. In contrast to the existing framework, this\n> > will result in a single executable for all test suites. The ability to\n> > pick specific tests to execute is retained via functionality built into\n> > the clar itself.\n> >\n> > Note that we need to be a bit careful about how we need to invalidate\n> > our Makefile rules. While we obviously have to regenerate the clar suite\n> > when our test suites change, we also have to invalidate it in case any\n> > of the test suites gets removed. We do so by using our typical pattern\n> > of creating a `GIT-TEST-SUITES` file that gets updated whenever the set\n> > of test suites changes, so that we can easily depend on that file.\n> >\n> > Another specialty is that we generate a \"clar-decls.h\" file. The test\n> > functions are neither static, nor do they have external declarations.\n> > This is because they are getting parsed via \"generate.py\", which then\n> > creates the external generations that get populated into an array. These\n> > declarations are only seen by the main function though.\n> >\n> > The consequence is that we will get a bunch of \"missing prototypes\"\n> > errors from our compiler for each of these test functions. To fix those\n> > errors, we extract the `extern` declarations from \"clar.suite\" and put\n> > them into a standalone header that then gets included by each of our\n> > unit tests. This gets rid of compiler warnings for every function which\n> > has been extracted by \"generate.py\". More importantly though, it does\n> > _not_ get rid of warnings in case a function really isn't being used by\n> > anything. Thus, it would cause a compiler error if a function name was\n> > mistyped and thus not picked up by \"generate.py\".\n> >\n> \n> > +$(UNIT_TEST_DIR)/clar.suite: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n> > +\t$(QUIET_GEN)$(UNIT_TEST_DIR)/clar/generate.py $(UNIT_TEST_DIR) >/dev/null\n> \n> This uses the Python interpreter from the shebang line in generate.py,\n> which is python.  On my system I only have python3 and python3.12, but\n> not python.  Easily fixed, of course, but a way to configure the\n> interpreter name would be nice.\n> \n> This gave me extra motivation to come up with the clunky patch below\n> to replace Python with sed and awk.  That and your statement that clar\n> doesn't have to be perfect in the other thread. ;)\n\nNeat! I would certainly prefer to not have a dependency on Python, and I\nthink awk(1) is a good alternative here that we already require anyway.\n\nAlso, another benefit of having our own script is that it allows us to\nbe more flexible with how exactly our tests are structured.\n\n> It reverses the order of dependencies (builds clar-decls.h first), not\n> sure if that has downsides.  And the sed pattern is simpler than the\n> one in generate.py, just out of laziness.\n\nWe could even integrate the generation of clar-decls.h with the AWK\nscript.\n\nI'll play around a bit with what you have, thanks a lot for working on\nit!\n\nPatrick\n"},{"id":"499786","messageId":"ZqtWHx_qIuDeFC8z@tanuki","threadId":"61874","inReplyTo":"xmqqmslx1bsu.fsf@gitster.g","subject":"Re: [RFC PATCH 2/3] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T09:32:15Z","receivedAt":"2024-08-01T09:32:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jul 31, 2024 at 02:39:13PM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > A trivial fix-up to be squashed into this step.\n> \n> I won't be able to address this today, but \"make sparse\" and other\n> auxiliary targets also seem to break, due to their lack of\n> dependence on the generated clar.suite file.\n\nYeah, as said in the cover letter, this was in quite an unpolished\nstate, so it is not surprising at all that things break. I just wanted\nto provide an unfinished PoC to demonstrate how things would roughly\nlook like so that we have something to discuss. Definitely not material\nfor `seen` yet.\n\nGiven that initial feedback didn't seem to be all that negative I'm\nhappy to polish things a bit more.\n\nPatrick\n"},{"id":"499841","messageId":"01c201dae40c$821ab5e0$865021a0$@nexbridge.com","threadId":"61874","inReplyTo":"ZqtWDtqkXzjz2A8u@tanuki","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T12:15:27Z","receivedAt":"2024-08-01T12:15:48Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 5:32 AM, Patrick Steinhardt wrote:\n>On Wed, Jul 31, 2024 at 04:04:29PM -0400, rsbecker@nexbridge.com wrote:\n>> On Wednesday, July 31, 2024 2:27 PM, Josh Steadmon wrote:\n>> >On 2024.07.31 11:04, Patrick Steinhardt wrote:\n>> >> Import the clar unit testing framework at commit faa8419 (Merge\n>> >> pull request #93 from clar-test/ethomson/fixtures, 2023-12-14). The\n>> >> framework will be wired up in subsequent commits.\n>> >\n>> >Rather than forking our own copy of clar, could we just add it as a\n>> submodule\n>> >instead?\n>>\n>> What are the requirements to build/use this?\n>\n>In its current form, Python is a dependency due to \"generate.py\". I\nmentioned\n>elsewhere though that I'd be happy to port it to our language of choice.\n>\n>Other than that it should only require a C89 compiler.\n\nUnfortunately, it needs gcc, but that is just because of its Makefile using\ncc -Wall (not portable).\n\n"},{"id":"499842","messageId":"024601dae411$faab2cb0$f0018610$@nexbridge.com","threadId":"61874","inReplyTo":"01c201dae40c$821ab5e0$865021a0$@nexbridge.com","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T12:54:36Z","receivedAt":"2024-08-01T12:54:53Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 8:15 AM, I wrote:\n>On Thursday, August 1, 2024 5:32 AM, Patrick Steinhardt wrote:\n>>On Wed, Jul 31, 2024 at 04:04:29PM -0400, rsbecker@nexbridge.com wrote:\n>>> On Wednesday, July 31, 2024 2:27 PM, Josh Steadmon wrote:\n>>> >On 2024.07.31 11:04, Patrick Steinhardt wrote:\n>>> >> Import the clar unit testing framework at commit faa8419 (Merge\n>>> >> pull request #93 from clar-test/ethomson/fixtures, 2023-12-14).\n>>> >> The framework will be wired up in subsequent commits.\n>>> >\n>>> >Rather than forking our own copy of clar, could we just add it as a\n>>> submodule\n>>> >instead?\n>>>\n>>> What are the requirements to build/use this?\n>>\n>>In its current form, Python is a dependency due to \"generate.py\". I\n>mentioned\n>>elsewhere though that I'd be happy to port it to our language of choice.\n>>\n>>Other than that it should only require a C89 compiler.\n>\n>Unfortunately, it needs gcc, but that is just because of its Makefile using\ncc -Wall\n>(not portable).\n\nIt looks like clar needs CC=c99 when running generate.py, which is fine. But\nthere is no current way to\nsuppress -Wall without hand-modifying clar/test/Makefile. The options like\nthat should either be in\nCFLAGS?=... instead of CFLAGS=... or we could introduce CFLAGS_DBG?= and put\n-Wall in there - it is\nonly for reporting warnings which is done different in the c99 I have.\nIdeally, the CFLAGS should come\nfrom ./git/config.mak.uname/\n\n"},{"id":"499843","messageId":"ZquPfiAWgYjIPGRB@tanuki","threadId":"61874","inReplyTo":"024601dae411$faab2cb0$f0018610$@nexbridge.com","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T13:37:02Z","receivedAt":"2024-08-01T13:37:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Aug 01, 2024 at 08:54:36AM -0400, rsbecker@nexbridge.com wrote:\n> On Thursday, August 1, 2024 8:15 AM, I wrote:\n> >On Thursday, August 1, 2024 5:32 AM, Patrick Steinhardt wrote:\n> >>On Wed, Jul 31, 2024 at 04:04:29PM -0400, rsbecker@nexbridge.com wrote:\n> >>> On Wednesday, July 31, 2024 2:27 PM, Josh Steadmon wrote:\n> >>> >On 2024.07.31 11:04, Patrick Steinhardt wrote:\n> >>> >> Import the clar unit testing framework at commit faa8419 (Merge\n> >>> >> pull request #93 from clar-test/ethomson/fixtures, 2023-12-14).\n> >>> >> The framework will be wired up in subsequent commits.\n> >>> >\n> >>> >Rather than forking our own copy of clar, could we just add it as a\n> >>> submodule\n> >>> >instead?\n> >>>\n> >>> What are the requirements to build/use this?\n> >>\n> >>In its current form, Python is a dependency due to \"generate.py\". I\n> >mentioned\n> >>elsewhere though that I'd be happy to port it to our language of choice.\n> >>\n> >>Other than that it should only require a C89 compiler.\n> >\n> >Unfortunately, it needs gcc, but that is just because of its Makefile using\n> cc -Wall\n> >(not portable).\n> \n> It looks like clar needs CC=c99 when running generate.py, which is fine. But\n> there is no current way to\n> suppress -Wall without hand-modifying clar/test/Makefile. The options like\n> that should either be in\n> CFLAGS?=... instead of CFLAGS=... or we could introduce CFLAGS_DBG?= and put\n> -Wall in there - it is\n> only for reporting warnings which is done different in the c99 I have.\n> Ideally, the CFLAGS should come\n> from ./git/config.mak.uname/\n\nAs mentioned in another mail, we do not use its Makefile at all. Did you\ncheck whether the version I have proposed here works when running `make\ntest`?\n\nPatrick\n"},{"id":"499844","messageId":"025f01dae419$636bb790$2a4326b0$@nexbridge.com","threadId":"61874","inReplyTo":"ZquPfiAWgYjIPGRB@tanuki","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T13:47:38Z","receivedAt":"2024-08-01T13:47:57Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n>On Thu, Aug 01, 2024 at 08:54:36AM -0400, rsbecker@nexbridge.com wrote:\n>> On Thursday, August 1, 2024 8:15 AM, I wrote:\n>> >On Thursday, August 1, 2024 5:32 AM, Patrick Steinhardt wrote:\n>> >>On Wed, Jul 31, 2024 at 04:04:29PM -0400, rsbecker@nexbridge.com wrote:\n>> >>> On Wednesday, July 31, 2024 2:27 PM, Josh Steadmon wrote:\n>> >>> >On 2024.07.31 11:04, Patrick Steinhardt wrote:\n>> >>> >> Import the clar unit testing framework at commit faa8419 (Merge\n>> >>> >> pull request #93 from clar-test/ethomson/fixtures, 2023-12-14).\n>> >>> >> The framework will be wired up in subsequent commits.\n>> >>> >\n>> >>> >Rather than forking our own copy of clar, could we just add it as\n>> >>> >a\n>> >>> submodule\n>> >>> >instead?\n>> >>>\n>> >>> What are the requirements to build/use this?\n>> >>\n>> >>In its current form, Python is a dependency due to \"generate.py\". I\n>> >mentioned\n>> >>elsewhere though that I'd be happy to port it to our language of\nchoice.\n>> >>\n>> >>Other than that it should only require a C89 compiler.\n>> >\n>> >Unfortunately, it needs gcc, but that is just because of its Makefile\n>> >using\n>> cc -Wall\n>> >(not portable).\n>>\n>> It looks like clar needs CC=c99 when running generate.py, which is\n>> fine. But there is no current way to suppress -Wall without\n>> hand-modifying clar/test/Makefile. The options like that should either\n>> be in CFLAGS?=... instead of CFLAGS=... or we could introduce\n>> CFLAGS_DBG?= and put -Wall in there - it is only for reporting\n>> warnings which is done different in the c99 I have.\n>> Ideally, the CFLAGS should come\n>> from ./git/config.mak.uname/\n>\n>As mentioned in another mail, we do not use its Makefile at all. Did you\ncheck\n>whether the version I have proposed here works when running `make test`?\n\nThat is the commit I have been trying to use. make test in clar or git? \n\n"},{"id":"499845","messageId":"ZquSkkR_aw2IUdX2@ncase","threadId":"61874","inReplyTo":"025f01dae419$636bb790$2a4326b0$@nexbridge.com","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T13:50:10Z","receivedAt":"2024-08-01T13:50:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Aug 01, 2024 at 09:47:38AM -0400, rsbecker@nexbridge.com wrote:\n> On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n> >As mentioned in another mail, we do not use its Makefile at all. Did you\n> check\n> >whether the version I have proposed here works when running `make test`?\n> \n> That is the commit I have been trying to use. make test in clar or git? \n\nIn Git itself. `make test` builds and runs our unit tests, and that now\nalso includes unit tests based on clar with this patch series. The clar\nMakefile exists only because I did a 1:1 import of the upstream\ndependency. We could just as well remove it altogether, including other\nbits that we don't end up using.\n\nPatrick\n"},{"id":"499846","messageId":"027301dae41a$41ea7ce0$c5bf76a0$@nexbridge.com","threadId":"61874","inReplyTo":"ZquSkkR_aw2IUdX2@ncase","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T13:53:52Z","receivedAt":"2024-08-01T13:54:07Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 9:50 AM, Patrick Steinhardt wrote:\n>On Thu, Aug 01, 2024 at 09:47:38AM -0400, rsbecker@nexbridge.com wrote:\n>> On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n>> >As mentioned in another mail, we do not use its Makefile at all. Did\n>> >you\n>> check\n>> >whether the version I have proposed here works when running `make test`?\n>>\n>> That is the commit I have been trying to use. make test in clar or git?\n>\n>In Git itself. `make test` builds and runs our unit tests, and that now\nalso includes\n>unit tests based on clar with this patch series. The clar Makefile exists\nonly because I\n>did a 1:1 import of the upstream dependency. We could just as well remove\nit\n>altogether, including other bits that we don't end up using.\n\nI see. Well, the 2.46.0 test passes. I ran 'seen' 5 days ago and 'next' is\ngoing now. Would that catch it?\n\n"},{"id":"499847","messageId":"ZquT0B-UXy73x6QI@ncase","threadId":"61874","inReplyTo":"027301dae41a$41ea7ce0$c5bf76a0$@nexbridge.com","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T13:55:28Z","receivedAt":"2024-08-01T13:55:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Aug 01, 2024 at 09:53:52AM -0400, rsbecker@nexbridge.com wrote:\n> On Thursday, August 1, 2024 9:50 AM, Patrick Steinhardt wrote:\n> >On Thu, Aug 01, 2024 at 09:47:38AM -0400, rsbecker@nexbridge.com wrote:\n> >> On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n> >> >As mentioned in another mail, we do not use its Makefile at all. Did\n> >> >you\n> >> check\n> >> >whether the version I have proposed here works when running `make test`?\n> >>\n> >> That is the commit I have been trying to use. make test in clar or git?\n> >\n> >In Git itself. `make test` builds and runs our unit tests, and that now\n> also includes\n> >unit tests based on clar with this patch series. The clar Makefile exists\n> only because I\n> >did a 1:1 import of the upstream dependency. We could just as well remove\n> it\n> >altogether, including other bits that we don't end up using.\n> \n> I see. Well, the 2.46.0 test passes. I ran 'seen' 5 days ago and 'next' is\n> going now. Would that catch it?\n\nNo, as the patches in this thread are only up for discussion right now\nand have not been merged to any of the branches. You'd have to apply\nthem on top of v2.46.0 first :)\n\nPatrick\n"},{"id":"499848","messageId":"028501dae41b$c277ec20$4767c460$@nexbridge.com","threadId":"61874","inReplyTo":"ZquT0B-UXy73x6QI@ncase","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T14:04:37Z","receivedAt":"2024-08-01T14:04:53Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 9:55 AM, Patrick Steinhardt wrote:\n>On Thu, Aug 01, 2024 at 09:53:52AM -0400, rsbecker@nexbridge.com wrote:\n>> On Thursday, August 1, 2024 9:50 AM, Patrick Steinhardt wrote:\n>> >On Thu, Aug 01, 2024 at 09:47:38AM -0400, rsbecker@nexbridge.com wrote:\n>> >> On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n>> >> >As mentioned in another mail, we do not use its Makefile at all.\n>> >> >Did you\n>> >> check\n>> >> >whether the version I have proposed here works when running `make\ntest`?\n>> >>\n>> >> That is the commit I have been trying to use. make test in clar or\ngit?\n>> >\n>> >In Git itself. `make test` builds and runs our unit tests, and that\n>> >now\n>> also includes\n>> >unit tests based on clar with this patch series. The clar Makefile\n>> >exists\n>> only because I\n>> >did a 1:1 import of the upstream dependency. We could just as well\n>> >remove\n>> it\n>> >altogether, including other bits that we don't end up using.\n>>\n>> I see. Well, the 2.46.0 test passes. I ran 'seen' 5 days ago and\n>> 'next' is going now. Would that catch it?\n>\n>No, as the patches in this thread are only up for discussion right now and\nhave not\n>been merged to any of the branches. You'd have to apply them on top of\nv2.46.0\n>first :)\n\nDo you happen to have a public fork?\nRegards,\nRandall\n\n"},{"id":"499851","messageId":"ZqufLYWnj75VeDXB@ncase","threadId":"61874","inReplyTo":"028501dae41b$c277ec20$4767c460$@nexbridge.com","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-01T14:43:57Z","receivedAt":"2024-08-01T14:44:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Aug 01, 2024 at 10:04:37AM -0400, rsbecker@nexbridge.com wrote:\n> On Thursday, August 1, 2024 9:55 AM, Patrick Steinhardt wrote:\n> >On Thu, Aug 01, 2024 at 09:53:52AM -0400, rsbecker@nexbridge.com wrote:\n> >> On Thursday, August 1, 2024 9:50 AM, Patrick Steinhardt wrote:\n> >> >On Thu, Aug 01, 2024 at 09:47:38AM -0400, rsbecker@nexbridge.com wrote:\n> >> >> On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n> >> >> >As mentioned in another mail, we do not use its Makefile at all.\n> >> >> >Did you\n> >> >> check\n> >> >> >whether the version I have proposed here works when running `make\n> test`?\n> >> >>\n> >> >> That is the commit I have been trying to use. make test in clar or\n> git?\n> >> >\n> >> >In Git itself. `make test` builds and runs our unit tests, and that\n> >> >now\n> >> also includes\n> >> >unit tests based on clar with this patch series. The clar Makefile\n> >> >exists\n> >> only because I\n> >> >did a 1:1 import of the upstream dependency. We could just as well\n> >> >remove\n> >> it\n> >> >altogether, including other bits that we don't end up using.\n> >>\n> >> I see. Well, the 2.46.0 test passes. I ran 'seen' 5 days ago and\n> >> 'next' is going now. Would that catch it?\n> >\n> >No, as the patches in this thread are only up for discussion right now and\n> have not\n> >been merged to any of the branches. You'd have to apply them on top of\n> v2.46.0\n> >first :)\n> \n> Do you happen to have a public fork?\n\nYou can pull the branch pks-clar-unit-tests from\nhttps://gitlab.com/gitlab-org/git.\n\nPatrick\n"},{"id":"499872","messageId":"02b901dae430$5461a720$fd24f560$@nexbridge.com","threadId":"61874","inReplyTo":"ZqufLYWnj75VeDXB@ncase","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T16:31:51Z","receivedAt":"2024-08-01T16:32:08Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 10:44 AM, Patrick Steinhardt wrote:\n>On Thu, Aug 01, 2024 at 10:04:37AM -0400, rsbecker@nexbridge.com wrote:\n>> On Thursday, August 1, 2024 9:55 AM, Patrick Steinhardt wrote:\n>> >On Thu, Aug 01, 2024 at 09:53:52AM -0400, rsbecker@nexbridge.com wrote:\n>> >> On Thursday, August 1, 2024 9:50 AM, Patrick Steinhardt wrote:\n>> >> >On Thu, Aug 01, 2024 at 09:47:38AM -0400, rsbecker@nexbridge.com\n>wrote:\n>> >> >> On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n>> >> >> >As mentioned in another mail, we do not use its Makefile at all.\n>> >> >> >Did you\n>> >> >> check\n>> >> >> >whether the version I have proposed here works when running\n>> >> >> >`make\n>> test`?\n>> >> >>\n>> >> >> That is the commit I have been trying to use. make test in clar\n>> >> >> or\n>> git?\n>> >> >\n>> >> >In Git itself. `make test` builds and runs our unit tests, and\n>> >> >that now\n>> >> also includes\n>> >> >unit tests based on clar with this patch series. The clar Makefile\n>> >> >exists\n>> >> only because I\n>> >> >did a 1:1 import of the upstream dependency. We could just as well\n>> >> >remove\n>> >> it\n>> >> >altogether, including other bits that we don't end up using.\n>> >>\n>> >> I see. Well, the 2.46.0 test passes. I ran 'seen' 5 days ago and\n>> >> 'next' is going now. Would that catch it?\n>> >\n>> >No, as the patches in this thread are only up for discussion right\n>> >now and\n>> have not\n>> >been merged to any of the branches. You'd have to apply them on top\n>> >of\n>> v2.46.0\n>> >first :)\n>>\n>> Do you happen to have a public fork?\n>\n>You can pull the branch pks-clar-unit-tests from\nhttps://gitlab.com/gitlab-org/git.\n\nTwo issues so far:\n\n    CC t/unit-tests/clar/clar.o\n                        assert(!\"Unexpected commandline argument!\");\n                        ^\n\"/home/randall/git-clar/t/unit-tests/clar/clar.c\", line 547: warning(317):\n          controlling expression is constant\n\n        if (mkdtemp(_clar_path) == NULL)\n            ^\n\"/home/randall/git-clar/t/unit-tests/clar/clar/sandbox.h\", line 132:\nerror(114):\n          identifier \"mkdtemp\" is undefined\n\nmkdtemp is not portable. assert was not #included. Thoughts?\n\n"},{"id":"499879","messageId":"02bf01dae435$3a5130d0$aef39270$@nexbridge.com","threadId":"61874","inReplyTo":"ZqufLYWnj75VeDXB@ncase","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T17:06:55Z","receivedAt":"2024-08-01T17:07:11Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 10:44 AM Patrick Steinhardt wrote:\n>On Thu, Aug 01, 2024 at 10:04:37AM -0400, rsbecker@nexbridge.com wrote:\n>> On Thursday, August 1, 2024 9:55 AM, Patrick Steinhardt wrote:\n>> >On Thu, Aug 01, 2024 at 09:53:52AM -0400, rsbecker@nexbridge.com wrote:\n>> >> On Thursday, August 1, 2024 9:50 AM, Patrick Steinhardt wrote:\n>> >> >On Thu, Aug 01, 2024 at 09:47:38AM -0400, rsbecker@nexbridge.com\n>wrote:\n>> >> >> On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n>> >> >> >As mentioned in another mail, we do not use its Makefile at all.\n>> >> >> >Did you\n>> >> >> check\n>> >> >> >whether the version I have proposed here works when running\n>> >> >> >`make\n>> test`?\n>> >> >>\n>> >> >> That is the commit I have been trying to use. make test in clar\n>> >> >> or\n>> git?\n>> >> >\n>> >> >In Git itself. `make test` builds and runs our unit tests, and\n>> >> >that now\n>> >> also includes\n>> >> >unit tests based on clar with this patch series. The clar Makefile\n>> >> >exists\n>> >> only because I\n>> >> >did a 1:1 import of the upstream dependency. We could just as well\n>> >> >remove\n>> >> it\n>> >> >altogether, including other bits that we don't end up using.\n>> >>\n>> >> I see. Well, the 2.46.0 test passes. I ran 'seen' 5 days ago and\n>> >> 'next' is going now. Would that catch it?\n>> >\n>> >No, as the patches in this thread are only up for discussion right\n>> >now and\n>> have not\n>> >been merged to any of the branches. You'd have to apply them on top\n>> >of\n>> v2.46.0\n>> >first :)\n>>\n>> Do you happen to have a public fork?\n>\n>You can pull the branch pks-clar-unit-tests from\nhttps://gitlab.com/gitlab-org/git.\n\nFix #1:\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h\nb/t/unit-tests/clar/clar/sandbox.h\nindex 0ba1479620..81a40f26b8 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -116,7 +116,7 @@ static int build_sandbox_path(void)\n\n        strncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n\n-#if defined(__MINGW32__)\n+#if defined(__MINGW32__) || defined(__TANDEM)\n        if (_mktemp(_clar_path) == NULL)\n                return -1;\n\nThat gets past the clar build problem. Probably more to come as I encounter\nissues. The assert issue was just a warning about a constant expression that\nwill always fail.\n\n"},{"id":"499882","messageId":"02cf01dae43a$61e95a50$25bc0ef0$@nexbridge.com","threadId":"61874","inReplyTo":"ZqufLYWnj75VeDXB@ncase","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework (better one)","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T17:43:49Z","receivedAt":"2024-08-01T17:44:06Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 10:44 AM, Patrick Steinhardt wrote:\n>On Thu, Aug 01, 2024 at 10:04:37AM -0400, rsbecker@nexbridge.com wrote:\n>> On Thursday, August 1, 2024 9:55 AM, Patrick Steinhardt wrote:\n>> >On Thu, Aug 01, 2024 at 09:53:52AM -0400, rsbecker@nexbridge.com wrote:\n>> >> On Thursday, August 1, 2024 9:50 AM, Patrick Steinhardt wrote:\n>> >> >On Thu, Aug 01, 2024 at 09:47:38AM -0400, rsbecker@nexbridge.com\n>wrote:\n>> >> >> On Thursday, August 1, 2024 9:37 AM, Patrick Steinhardt wrote:\n>> >> >> >As mentioned in another mail, we do not use its Makefile at all.\n>> >> >> >Did you\n>> >> >> check\n>> >> >> >whether the version I have proposed here works when running\n>> >> >> >`make\n>> test`?\n>> >> >>\n>> >> >> That is the commit I have been trying to use. make test in clar\n>> >> >> or\n>> git?\n>> >> >\n>> >> >In Git itself. `make test` builds and runs our unit tests, and\n>> >> >that now\n>> >> also includes\n>> >> >unit tests based on clar with this patch series. The clar Makefile\n>> >> >exists\n>> >> only because I\n>> >> >did a 1:1 import of the upstream dependency. We could just as well\n>> >> >remove\n>> >> it\n>> >> >altogether, including other bits that we don't end up using.\n>> >>\n>> >> I see. Well, the 2.46.0 test passes. I ran 'seen' 5 days ago and\n>> >> 'next' is going now. Would that catch it?\n>> >\n>> >No, as the patches in this thread are only up for discussion right\n>> >now and\n>> have not\n>> >been merged to any of the branches. You'd have to apply them on top\n>> >of\n>> v2.46.0\n>> >first :)\n>>\n>> Do you happen to have a public fork?\n>\n>You can pull the branch pks-clar-unit-tests from\nhttps://gitlab.com/gitlab-org/git.\n\nThis fix is better (and actually works unlike the previous):\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h\nb/t/unit-tests/clar/clar/sandbox.h\nindex 0ba1479620..1b14dbfd76 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -120,6 +120,12 @@ static int build_sandbox_path(void)\n        if (_mktemp(_clar_path) == NULL)\n                return -1;\n\n+       if (mkdir(_clar_path, 0700) != 0)\n+               return -1;\n+#elif defined(__TANDEM)\n+       if (mktemp(_clar_path) == NULL)\n+               return -1;\n+\n        if (mkdir(_clar_path, 0700) != 0)\n                return -1;\n #elif defined(_WIN32)\n\nThis is based on the above repo/branch. Basically a separate block for\n__TANDEM for temp directories. Build is done. I'm running the full test now\nand may report more or success.\nRegards,\nRandall\n\n\n"},{"id":"499884","messageId":"3e31de95-dab7-4552-9a39-1266879a1f86@web.de","threadId":"61874","inReplyTo":"02cf01dae43a$61e95a50$25bc0ef0$@nexbridge.com","subject":"Re: [RFC PATCH 1/3] t: import the clar unit testing framework (better one)","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2024-08-01T18:12:32Z","receivedAt":"2024-08-01T18:12:56Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 01.08.24 um 19:43 schrieb rsbecker@nexbridge.com:\n>\n> I'm running the full test now and may report more or success.\n\n\"make unit-tests\" would suffice.\n\nRené\n"},{"id":"499886","messageId":"02e001dae441$56c502d0$044f0870$@nexbridge.com","threadId":"61874","inReplyTo":"3e31de95-dab7-4552-9a39-1266879a1f86@web.de","subject":"RE: [RFC PATCH 1/3] t: import the clar unit testing framework (better one)","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-01T18:33:37Z","receivedAt":"2024-08-01T18:33:52Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Thursday, August 1, 2024 2:13 PM, René Scharfe wrote:\n>Am 01.08.24 um 19:43 schrieb rsbecker@nexbridge.com:\n>>\n>> I'm running the full test now and may report more or success.\n>\n>\"make unit-tests\" would suffice.\n\nWell, that takes a whole lot less time, thanks. 100% pass after the prior fix was applied.\n\nThank you.\nRandall\n\n"},{"id":"500194","messageId":"cover.1722952908.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[RFC PATCH v2 0/7] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-06T14:14:30Z","receivedAt":"2024-08-06T14:14:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the second version of my RFC patch series that introduces the\nclar testing framework into our unit tests. The intent is to not have to\nhand-craft all features of a proper unit testing framework, while still\nnot painting us into a corner. As such, the clar itself is small and\nextensible while still bringing some nice features to the table.\n\nChanges compared to v1:\n\n  - Convert the ctypes unit tests to use clar, as I had it lying around\n    anyway and René was asking for it.\n\n  - Adopt the AWK-based script by René, replacing the Python-based one.\n    This gets rid of the mandatory Python dependency and at the same\n    time also allows us to be more flexible going forward.\n\n  - Wire up the third party sources in our Makefiles such that they do\n    not get linted via hdr-check or Coccinelle.\n\n  - Fix t/Makefile to not pass GIT_TEST_OPTS to our unit tests. They\n    don't know how to handle those options, and clar-based tests error\n    out when they see unknown options.\n\n  - Adapt Documentation/technical/unit-tests.txt to mention the clar.\n\n  - Rebase the series onto the latest master at 406f326d27 (The second\n    batch, 2024-08-01) to avoid some conflicts.\n\n  - Cherry-pick a whitespace fix that otherwise makes git-apply(1)\n    unhappy. The CI job is still broken because the first patch that\n    imports clar continues to be broken. I've created an upstream PR.\n\n  - Cherry-pick a fix by Randall to make clar work on HP NonStop. The\n    fix has been proposed upstream in a PR.\n\nWith these changes, the CI jobs at GitLab all pass, except for the\nwhitespace and clang-format checks.\n\nI've also Cc'd Ed, one of the maintainers of clar. I also noticed that\nI'm a maintainer of that project back from my libgit2 times, so I think\nit should be relatively easy to land changes upstream.\n\nThanks!\n\nPatrick\n\nPatrick Steinhardt (7):\n  t: do not pass GIT_TEST_OPTS to unit tests with prove\n  t: import the clar unit testing framework\n  t/clar: fix whitespace errors\n  t/clar: fix compatibility with NonStop\n  Makefile: wire up the clar unit testing framework\n  t/unit-tests: convert strvec tests to use clar\n  t/unit-tests: convert ctype tests to use clar\n\n .gitignore                                 |   1 +\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |  42 +-\n t/Makefile                                 |   4 +-\n t/run-test.sh                              |   2 +-\n t/unit-tests/.gitignore                    |   2 +\n t/unit-tests/clar-generate.awk             |  50 ++\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n t/unit-tests/{t-strvec.c => strvec.c}      | 119 ++-\n t/unit-tests/unit-test.c                   |  17 +\n t/unit-tests/unit-test.h                   |   3 +\n 29 files changed, 3159 insertions(+), 98 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\nRange-diff against v1:\n-:  ---------- > 1:  78a9cc1162 t: do not pass GIT_TEST_OPTS to unit tests with prove\n1:  4e3862991a ! 2:  6a88cf22a5 t: import the clar unit testing framework\n    @@ Commit message\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    + ## Documentation/technical/unit-tests.txt ##\n    +@@ Documentation/technical/unit-tests.txt: GitHub / GitLab stars to estimate this.\n    + :criterion: https://github.com/Snaipe/Criterion[Criterion]\n    + :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n    + :check: https://libcheck.github.io/check/[Check]\n    ++:clar: https://github.com/clar-test/clar[Clar]\n    + \n    + [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n    + |=====\n    +@@ Documentation/technical/unit-tests.txt: Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n    + {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n    + {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n    + {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n    ++{clar},{lgpl},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n    + |=====\n    + \n    + === Additional framework candidates\n    +\n    + ## Makefile ##\n    +@@ Makefile: THIRD_PARTY_SOURCES += compat/poll/%\n    + THIRD_PARTY_SOURCES += compat/regex/%\n    + THIRD_PARTY_SOURCES += sha1collisiondetection/%\n    + THIRD_PARTY_SOURCES += sha1dc/%\n    ++THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n    ++THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n    + \n    + UNIT_TEST_PROGRAMS += t-ctype\n    + UNIT_TEST_PROGRAMS += t-example-decorate\n    +@@ Makefile: $(SP_OBJ): %.sp: %.c %.o\n    + .PHONY: sparse\n    + sparse: $(SP_OBJ)\n    + \n    +-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n    ++EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n    + ifndef OPENSSL_SHA1\n    + \tEXCEPT_HDRS += sha1/openssl.h\n    + endif\n    +\n      ## t/unit-tests/clar/.github/workflows/ci.yml (new) ##\n     @@\n     +name: CI\n-:  ---------- > 3:  a52ee59bf4 t/clar: fix whitespace errors\n-:  ---------- > 4:  02fb86dfbc t/clar: fix compatibility with NonStop\n2:  7a5dfd5065 ! 5:  848dc673c4 Makefile: wire up the clar unit testing framework\n    @@ .gitignore\n      /bin-wrappers/\n     \n      ## Makefile ##\n    -@@ Makefile: THIRD_PARTY_SOURCES += compat/regex/%\n    - THIRD_PARTY_SOURCES += sha1collisiondetection/%\n    - THIRD_PARTY_SOURCES += sha1dc/%\n    +@@ Makefile: THIRD_PARTY_SOURCES += sha1dc/%\n    + THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n    + THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n      \n     +UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n     +UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n    @@ Makefile: endif\n      \n      bin-wrappers/%: wrap-for-bin.sh\n      \t$(call mkdir_p_parent_template)\n    +@@ Makefile: $(SP_OBJ): %.sp: %.c %.o\n    + .PHONY: sparse\n    + sparse: $(SP_OBJ)\n    + \n    +-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n    ++EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/unit-test.h $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n    + ifndef OPENSSL_SHA1\n    + \tEXCEPT_HDRS += sha1/openssl.h\n    + endif\n     @@ Makefile: endif\n      \n      artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n    @@ Makefile: cocciclean:\n      \n      clean: profile-clean coverage-clean cocciclean\n      \t$(RM) -r .build $(UNIT_TEST_BIN)\n    -+\t$(RM) $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/.clarcache\n    ++\t$(RM) GIT-TEST-SUITES $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/clar-decls.h\n      \t$(RM) po/git.pot po/git-core.pot\n      \t$(RM) git.res\n      \t$(RM) $(OBJECTS)\n    @@ Makefile: $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n     +\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n     +\t\techo >&2 \"    * new test suites\"; \\\n     +\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n    -+\t\trm -f $(UNIT_TESTS_DIR)/.clarcache; \\\n     +            fi\n     +\n    -+$(UNIT_TEST_DIR)/clar.suite: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n    -+\t$(QUIET_GEN)$(UNIT_TEST_DIR)/clar/generate.py $(UNIT_TEST_DIR) >/dev/null\n    -+\t@touch $@\n    -+$(UNIT_TEST_DIR)/clar-decls.h: $(UNIT_TEST_DIR)/clar.suite\n    -+\t$(QUIET_GEN)grep '^extern void' $^ >$@\n    ++$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n    ++\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n    ++\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n    ++\tdone >$@\n    ++$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n    ++\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n     +$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n     +$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR) -I$(UNIT_TEST_DIR)/clar\n     +$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n    @@ t/Makefile: CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard cha\n      ## t/unit-tests/.gitignore ##\n     @@\n      /bin\n    -+/.clarcache\n     +/clar.suite\n     +/clar-decls.h\n     \n    + ## t/unit-tests/clar-generate.awk (new) ##\n    +@@\n    ++function add_suite(suite, initialize, cleanup, count) {\n    ++       if (!suite) return\n    ++       suite_count++\n    ++       callback_count += count\n    ++       suites = suites \"    {\\n\"\n    ++       suites = suites \"        \\\"\" suite \"\\\",\\n\"\n    ++       suites = suites \"        \" initialize \",\\n\"\n    ++       suites = suites \"        \" cleanup \",\\n\"\n    ++       suites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n    ++       suites = suites \"    },\\n\"\n    ++}\n    ++\n    ++BEGIN {\n    ++       suites = \"static struct clar_suite _clar_suites[] = {\\n\"\n    ++}\n    ++\n    ++{\n    ++       print\n    ++       name = $3; sub(/\\(.*$/, \"\", name)\n    ++       suite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n    ++       short_name = name; sub(/^.*__/, \"\", short_name)\n    ++       cb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n    ++       if (suite != prev_suite) {\n    ++               add_suite(prev_suite, initialize, cleanup, count)\n    ++               if (callbacks) callbacks = callbacks \"};\\n\"\n    ++               callbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n    ++               initialize = \"{ NULL, NULL }\"\n    ++               cleanup = \"{ NULL, NULL }\"\n    ++               count = 0\n    ++               prev_suite = suite\n    ++       }\n    ++       if (short_name == \"initialize\") {\n    ++               initialize = cb\n    ++       } else if (short_name == \"cleanup\") {\n    ++               cleanup = cb\n    ++       } else {\n    ++               callbacks = callbacks \"    \" cb \",\\n\"\n    ++               count++\n    ++       }\n    ++}\n    ++\n    ++END {\n    ++       add_suite(suite, initialize, cleanup, count)\n    ++       suites = suites \"};\"\n    ++       if (callbacks) callbacks = callbacks \"};\"\n    ++       print callbacks\n    ++       print suites\n    ++       print \"static const size_t _clar_suite_count = \" suite_count \";\"\n    ++       print \"static const size_t _clar_callback_count = \" callback_count \";\"\n    ++}\n    +\n      ## t/unit-tests/unit-test.c (new) ##\n     @@\n     +#include \"unit-test.h\"\n     +\n     +int cmd_main(int argc, const char **argv)\n     +{\n    -+\tconst char **args;\n    ++\tconst char **argv_copy;\n     +\tint ret;\n     +\n     +\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n    -+\tDUP_ARRAY(args, argv, argc + 1);\n    -+\targs[argc++] = \"-t\";\n    ++\tALLOC_ARRAY(argv_copy, argc + 2);\n    ++\tCOPY_ARRAY(argv_copy, argv, argc);\n    ++\targv_copy[argc++] = \"-t\";\n    ++\targv_copy[argc] = NULL;\n     +\n    -+\tret = clar_test(argc, (char **) args);\n    ++\tret = clar_test(argc, (char **) argv_copy);\n     +\n    -+\tfree(args);\n    ++\tfree(argv_copy);\n     +\treturn ret;\n     +}\n     \n3:  1c2a510547 ! 6:  578e657269 t/unit-tests: convert strvec tests to use clar\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Makefile ##\n    -@@ Makefile: THIRD_PARTY_SOURCES += compat/regex/%\n    - THIRD_PARTY_SOURCES += sha1collisiondetection/%\n    - THIRD_PARTY_SOURCES += sha1dc/%\n    +@@ Makefile: THIRD_PARTY_SOURCES += sha1dc/%\n    + THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n    + THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n      \n     +UNIT_TESTS_SUITES += strvec\n      UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n      UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n      UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n    -@@ Makefile: UNIT_TEST_PROGRAMS += t-reftable-basics\n    +@@ Makefile: UNIT_TEST_PROGRAMS += t-reftable-merged\n      UNIT_TEST_PROGRAMS += t-reftable-record\n      UNIT_TEST_PROGRAMS += t-strbuf\n      UNIT_TEST_PROGRAMS += t-strcmp-offset\n    @@ t/unit-tests/t-strvec.c => t/unit-tests/strvec.c\n      #include \"strvec.h\"\n      \n      #define check_strvec(vec, ...) \\\n    --\tcheck_strvec_loc(TEST_LOCATION(), vec, __VA_ARGS__)\n    -+\tcheck_strvec_loc(__FILE__, __func__, __LINE__, vec, __VA_ARGS__)\n    - LAST_ARG_MUST_BE_NULL\n    --static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n    -+static void check_strvec_loc(const char *file, const char *func, size_t line, struct strvec *vec, ...)\n    - {\n    - \tva_list ap;\n    - \tsize_t nr = 0;\n    -@@ t/unit-tests/strvec.c: static void check_strvec_loc(const char *loc, struct strvec *vec, ...)\n    - \t\tif (!str)\n    - \t\t\tbreak;\n    - \n    --\t\tif (!check_uint(vec->nr, >, nr) ||\n    --\t\t    !check_uint(vec->alloc, >, nr) ||\n    --\t\t    !check_str(vec->v[nr], str)) {\n    --\t\t\tstruct strbuf msg = STRBUF_INIT;\n    --\t\t\tstrbuf_addf(&msg, \"strvec index %\"PRIuMAX, (uintmax_t) nr);\n    --\t\t\ttest_assert(loc, msg.buf, 0);\n    --\t\t\tstrbuf_release(&msg);\n    --\t\t\tva_end(ap);\n    --\t\t\treturn;\n    --\t\t}\n    -+\t\tclar__assert(vec->nr > nr, file, func, line, \"Vector size too small\", NULL, 1);\n    -+\t\tclar__assert(vec->alloc > nr, file, func, line, \"Vector allocation too small\", NULL, 1);\n    -+\t\tcl_assert_equal_s(vec->v[nr], str);\n    - \n    - \t\tnr++;\n    - \t}\n    - \tva_end(ap);\n    - \n    --\tcheck_uint(vec->nr, ==, nr);\n    --\tcheck_uint(vec->alloc, >=, nr);\n    --\tcheck_pointer_eq(vec->v[nr], NULL);\n    -+\tcl_assert(vec->nr == nr);\n    -+\tcl_assert(vec->alloc >= nr);\n    -+\tcl_assert_equal_p(vec->v[nr], NULL);\n    - }\n    + \tdo { \\\n    + \t\tconst char *expect[] = { __VA_ARGS__ }; \\\n    +-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n    +-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n    +-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n    +-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n    +-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n    +-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n    +-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n    +-\t\t\t\t\t\t (uintmax_t)i); \\\n    +-\t\t\t\t\tbreak; \\\n    +-\t\t\t\t} \\\n    +-\t\t\t} \\\n    +-\t\t} \\\n    ++\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n    ++\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n    ++\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n    ++\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n    ++\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n    ++\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n    + \t} while (0)\n      \n     -static void t_static_init(void)\n     +void test_strvec__init(void)\n    @@ t/unit-tests/strvec.c: static void t_push(void)\n      }\n      \n     -static void t_pushf(void)\n    -+void test_strvec__pushft_pushf(void)\n    ++void test_strvec__pushf(void)\n      {\n      \tstruct strvec vec = STRVEC_INIT;\n      \tstrvec_pushf(&vec, \"foo: %d\", 1);\n    @@ t/unit-tests/strvec.c: static void t_detach(void)\n     -\tTEST(t_detach(), \"detach\");\n     -\treturn test_done();\n     -}\n    +\n    + ## t/unit-tests/unit-test.c ##\n    +@@ t/unit-tests/unit-test.c: int cmd_main(int argc, const char **argv)\n    + \tint ret;\n    + \n    + \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n    +-\tALLOC_ARRAY(argv_copy, argc + 2);\n    ++\tALLOC_ARRAY(argv_copy, argc + 1);\n    + \tCOPY_ARRAY(argv_copy, argv, argc);\n    + \targv_copy[argc++] = \"-t\";\n    +-\targv_copy[argc] = NULL;\n    + \n    + \tret = clar_test(argc, (char **) argv_copy);\n    + \n-:  ---------- > 7:  238de33b93 t/unit-tests: convert ctype tests to use clar\n-- \n2.46.0.dirty\n\n"},{"id":"500195","messageId":"78a9cc116253c8c638cc430bd0a842805105648b.1722952908.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722952908.git.ps@pks.im","subject":"[RFC PATCH v2 1/7] t: do not pass GIT_TEST_OPTS to unit tests with prove","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-06T14:14:34Z","receivedAt":"2024-08-06T14:14:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When using the prove target, we append GIT_TEST_OPTS to the arguments\nthat we execute each of the tests with. This doesn't only include the\nintended test scripts, but also ends up passing the arguments to our\nunit tests. This is unintentional though as they do not even know to\ninterpret those arguments, and is inconsistent with how we execute unit\ntests without prove.\n\nThis isn't much of an issue because our current set of unit tests mostly\nignore their arguments anyway. With the introduction of clar-based unit\ntests this is about to become an issue though, as these do parse their\ncommand line argument to alter behaviour.\n\nPrepare for this by passing GIT_TEST_OPTS to \"run-test.sh\" via an\nenvironment variable. Like this, we can conditionally forward it to our\ntest scripts, only.\n---\n t/Makefile    | 3 ++-\n t/run-test.sh | 2 +-\n 2 files changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/Makefile b/t/Makefile\nindex 4c30e7c06f..d2212de0b7 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -68,7 +68,8 @@ failed:\n \ttest -z \"$$failed\" || $(MAKE) $$failed\n \n prove: pre-clean check-chainlint $(TEST_LINT)\n-\t@echo \"*** prove (shell & unit tests) ***\"; $(CHAINLINTSUPPRESS) TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS) :: $(GIT_TEST_OPTS)\n+\t@echo \"*** prove (shell & unit tests) ***\"\n+\t@$(CHAINLINTSUPPRESS) TEST_OPTIONS='$(GIT_TEST_OPTS)' TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS)\n \t$(MAKE) clean-except-prove-cache\n \n $(T):\ndiff --git a/t/run-test.sh b/t/run-test.sh\nindex 13c353b91b..63328ac630 100755\n--- a/t/run-test.sh\n+++ b/t/run-test.sh\n@@ -10,7 +10,7 @@ case \"$1\" in\n \t\techo >&2 \"ERROR: TEST_SHELL_PATH is empty or not set\"\n \t\texit 1\n \tfi\n-\texec \"${TEST_SHELL_PATH}\" \"$@\"\n+\texec \"${TEST_SHELL_PATH}\" \"$@\" ${TEST_OPTIONS}\n \t;;\n *)\n \texec \"$@\"\n-- \n2.46.0.dirty\n\n"},{"id":"500196","messageId":"6a88cf22a5de3faa77cb22998d27f0c1b63bd4ff.1722952908.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722952908.git.ps@pks.im","subject":"[RFC PATCH v2 2/7] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-06T14:14:39Z","receivedAt":"2024-08-06T14:14:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Import the clar unit testing framework at commit faa8419 (Merge pull\nrequest #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |   4 +-\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 154 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 267 +++++++\n t/unit-tests/clar/test/.gitignore          |   5 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n 20 files changed, 2946 insertions(+), 1 deletion(-)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n\ndiff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt\nindex 206037ffb1..fc201d4b3d 100644\n--- a/Documentation/technical/unit-tests.txt\n+++ b/Documentation/technical/unit-tests.txt\n@@ -203,6 +203,7 @@ GitHub / GitLab stars to estimate this.\n :criterion: https://github.com/Snaipe/Criterion[Criterion]\n :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n :check: https://libcheck.github.io/check/[Check]\n+:clar: https://github.com/clar-test/clar[Clar]\n \n [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n |=====\n@@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n+{clar},{lgpl},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n |=====\n \n === Additional framework candidates\ndiff --git a/Makefile b/Makefile\nindex 3863e60b66..674b0ac4e1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1331,6 +1331,8 @@ THIRD_PARTY_SOURCES += compat/poll/%\n THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n@@ -3260,7 +3262,7 @@ $(SP_OBJ): %.sp: %.c %.o\n .PHONY: sparse\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n ifndef OPENSSL_SHA1\n \tEXCEPT_HDRS += sha1/openssl.h\n endif\ndiff --git a/t/unit-tests/clar/.github/workflows/ci.yml b/t/unit-tests/clar/.github/workflows/ci.yml\nnew file mode 100644\nindex 0000000000..b1ac2de460\n--- /dev/null\n+++ b/t/unit-tests/clar/.github/workflows/ci.yml\n@@ -0,0 +1,23 @@\n+name: CI\n+\n+on:\n+  push:\n+    branches: [ main ]\n+  pull_request:\n+    branches: [ main ]\n+\n+jobs:\n+  build:\n+    strategy:\n+      matrix:\n+        os: [ ubuntu-latest, macos-latest ]\n+\n+    runs-on: ${{ matrix.os }}\n+\n+    steps:\n+    - name: Check out\n+      uses: actions/checkout@v2\n+    - name: Build\n+      run: |\n+        cd test\n+        make\ndiff --git a/t/unit-tests/clar/COPYING b/t/unit-tests/clar/COPYING\nnew file mode 100644\nindex 0000000000..8983817f0c\n--- /dev/null\n+++ b/t/unit-tests/clar/COPYING\n@@ -0,0 +1,15 @@\n+ISC License\n+\n+Copyright (c) 2011-2015 Vicent Marti\n+\n+Permission to use, copy, modify, and/or distribute this software for any\n+purpose with or without fee is hereby granted, provided that the above\n+copyright notice and this permission notice appear in all copies.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ndiff --git a/t/unit-tests/clar/README.md b/t/unit-tests/clar/README.md\nnew file mode 100644\nindex 0000000000..a8961c5f10\n--- /dev/null\n+++ b/t/unit-tests/clar/README.md\n@@ -0,0 +1,329 @@\n+Come out and Clar\n+=================\n+\n+In Catalan, \"clar\" means clear, easy to perceive.  Using clar will make it\n+easy to test and make clear the quality of your code.\n+\n+> _Historical note_\n+>\n+> Originally the clar project was named \"clay\" because the word \"test\" has its\n+> roots in the latin word *\"testum\"*, meaning \"earthen pot\", and *\"testa\"*,\n+> meaning \"piece of burned clay\"?\n+>\n+> This is because historically, testing implied melting metal in a pot to\n+> check its quality.  Clay is what tests are made of.\n+\n+## Quick Usage Overview\n+\n+Clar is a minimal C unit testing framework. It's been written to replace the\n+old framework in [libgit2][libgit2], but it's both very versatile and\n+straightforward to use.\n+\n+Can you count to funk?\n+\n+- **Zero: Initialize test directory**\n+\n+    ~~~~ sh\n+    $ mkdir tests\n+    $ cp -r $CLAR_ROOT/clar* tests\n+    $ cp $CLAR_ROOT/test/clar_test.h tests\n+    $ cp $CLAR_ROOT/test/main.c.sample tests/main.c\n+    ~~~~\n+\n+- **One: Write some tests**\n+\n+    File: tests/adding.c:\n+\n+    ~~~~ c\n+    /* adding.c for the \"Adding\" suite */\n+    #include \"clar.h\"\n+\n+    static int *answer;\n+\n+    void test_adding__initialize(void)\n+    {\n+        answer = malloc(sizeof(int));\n+        cl_assert_(answer != NULL, \"No memory left?\");\n+        *answer = 42;\n+    }\n+\n+    void test_adding__cleanup(void)\n+    {\n+        free(answer);\n+    }\n+\n+    void test_adding__make_sure_math_still_works(void)\n+    {\n+        cl_assert_(5 > 3, \"Five should probably be greater than three\");\n+        cl_assert_(-5 < 2, \"Negative numbers are small, I think\");\n+        cl_assert_(*answer == 42, \"The universe is doing OK. And the initializer too.\");\n+    }\n+    ~~~~~\n+\n+- **Two: Build the test executable**\n+\n+    ~~~~ sh\n+    $ cd tests\n+    $ $CLAR_PATH/generate.py .\n+    Written `clar.suite` (1 suites)\n+    $ gcc -I. clar.c main.c adding.c -o testit\n+    ~~~~\n+\n+- **Funk: Funk it.**\n+\n+    ~~~~ sh\n+    $ ./testit\n+    ~~~~\n+\n+## The Clar Test Suite\n+\n+Writing a test suite is pretty straightforward. Each test suite is a `*.c`\n+file with a descriptive name: this encourages modularity.\n+\n+Each test suite has optional initialize and cleanup methods. These methods\n+will be called before and after running **each** test in the suite, even if\n+such test fails. As a rule of thumb, if a test needs a different initializer\n+or cleanup method than another test in the same module, that means it\n+doesn't belong in that module. Keep that in mind when grouping tests\n+together.\n+\n+The `initialize` and `cleanup` methods have the following syntax, with\n+`suitename` being the current suite name, e.g. `adding` for the `adding.c`\n+suite.\n+\n+~~~~ c\n+void test_suitename__initialize(void)\n+{\n+    /* init */\n+}\n+\n+void test_suitename__cleanup(void)\n+{\n+    /* cleanup */\n+}\n+~~~~\n+\n+These methods are encouraged to use static, global variables to store the state\n+that will be used by all tests inside the suite.\n+\n+~~~~ c\n+static git_repository *_repository;\n+\n+void test_status__initialize(void)\n+{\n+    create_tmp_repo(STATUS_REPO);\n+    git_repository_open(_repository, STATUS_REPO);\n+}\n+\n+void test_status__cleanup(void)\n+{\n+    git_repository_close(_repository);\n+    git_path_rm(STATUS_REPO);\n+}\n+\n+void test_status__simple_test(void)\n+{\n+    /* do something with _repository */\n+}\n+~~~~\n+\n+Writing the actual tests is just as straightforward. Tests have the\n+`void test_suitename__test_name(void)` signature, and they should **not**\n+be static. Clar will automatically detect and list them.\n+\n+Tests are run as they appear on their original suites: they have no return\n+value. A test is considered \"passed\" if it doesn't raise any errors. Check\n+the \"Clar API\" section to see the various helper functions to check and\n+raise errors during test execution.\n+\n+__Caution:__ If you use assertions inside of `test_suitename__initialize`,\n+make sure that you do not rely on `__initialize` being completely run\n+inside your `test_suitename__cleanup` function. Otherwise you might\n+encounter ressource cleanup twice.\n+\n+## How does Clar work?\n+\n+To use Clar:\n+\n+1. copy the Clar boilerplate to your test directory\n+2. copy (and probably modify) the sample `main.c` (from\n+   `$CLAR_PATH/test/main.c.sample`)\n+3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and\n+   write out the test suite metadata.\n+4. compile your test files and the Clar boilerplate into a single test\n+   executable\n+5. run the executable to test!\n+\n+The Clar boilerplate gives you a set of useful test assertions and features\n+(like accessing or making sandbox copies of fixture data).  It consists of\n+the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory.\n+You should not need to edit these files.\n+\n+The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes\n+`clar_test(argc, argv)` to run the tests.  Usually, you will edit this file\n+to perform any framework specific initialization and teardown that you need.\n+\n+The Clar mixer (`generate.py`) recursively scans your test directory for\n+any `.c` files, parses them, and writes the `clar.suite` file with all of\n+the metadata about your tests.  When you build, the `clar.suite` file is\n+included into `clar.c`.\n+\n+The mixer can be run with **Python 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and PyPy 1.6**.\n+\n+Commandline usage of the mixer is as follows:\n+\n+    $ ./generate.py .\n+\n+Where `.` is the folder where all the test suites can be found. The mixer\n+will automatically locate all the relevant source files and build the\n+testing metadata. The metadata will be written to `clar.suite`, in the same\n+folder as all the test suites. This file is included by `clar.c` and so\n+must be accessible via `#include` when building the test executable.\n+\n+    $ gcc -I. clar.c main.c suite1.c test2.c -o run_tests\n+\n+**Note that the Clar mixer only needs to be ran when adding new tests to a\n+suite, in order to regenerate the metadata**. As a result, the `clar.suite`\n+file can be checked into version control if you wish to be able to build\n+your test suite without having to re-run the mixer.\n+\n+This is handy when e.g. generating tests in a local computer, and then\n+building and testing them on an embedded device or a platform where Python\n+is not available.\n+\n+### Fixtures\n+\n+Clar can create sandboxed fixtures for you to use in your test. You'll need to compile *clar.c* with an additional `CFLAG`, `-DCLAR_FIXTURE_PATH`. This should be an absolute path to your fixtures directory.\n+\n+Once that's done, you can use the fixture API as defined below.\n+\n+## The Clar API\n+\n+Clar makes the following methods available from all functions in a test\n+suite.\n+\n+-   `cl_must_pass(call)`, `cl_must_pass_(call, message)`: Verify that the given\n+    function call passes, in the POSIX sense (returns a value greater or equal\n+    to 0).\n+\n+-   `cl_must_fail(call)`, `cl_must_fail_(call, message)`: Verify that the given\n+    function call fails, in the POSIX sense (returns a value less than 0).\n+\n+-   `cl_assert(expr)`, `cl_assert_(expr, message)`: Verify that `expr` is true.\n+\n+-   `cl_check_pass(call)`, `cl_check_pass_(call, message)`: Verify that the\n+    given function call passes, in the POSIX sense (returns a value greater or\n+    equal to 0). If the function call doesn't succeed, a test failure will be\n+    logged but the test's execution will continue.\n+\n+-   `cl_check_fail(call)`, `cl_check_fail_(call, message)`: Verify that the\n+    given function call fails, in the POSIX sense (returns a value less than\n+    0). If the function call doesn't fail, a test failure will be logged but\n+    the test's execution will continue.\n+\n+-   `cl_check(expr)`: Verify that `expr` is true. If `expr` is not\n+    true, a test failure will be logged but the test's execution will continue.\n+\n+-   `cl_fail(message)`: Fail the current test with the given message.\n+\n+-   `cl_warning(message)`: Issue a warning. This warning will be\n+    logged as a test failure but the test's execution will continue.\n+\n+-   `cl_set_cleanup(void (*cleanup)(void *), void *opaque)`: Set the cleanup\n+    method for a single test. This method will be called with `opaque` as its\n+    argument before the test returns (even if the test has failed).\n+    If a global cleanup method is also available, the local cleanup will be\n+    called first, and then the global.\n+\n+-   `cl_assert_equal_i(int,int)`: Verify that two integer values are equal.\n+    The advantage of this over a simple `cl_assert` is that it will format\n+    a much nicer error report if the values are not equal.\n+\n+-   `cl_assert_equal_s(const char *,const char *)`: Verify that two strings\n+    are equal.  The expected value can also be NULL and this will correctly\n+    test for that.\n+\n+-   `cl_fixture_sandbox(const char *)`: Sets up a sandbox for a fixture\n+    so that you can mutate the file directly.\n+\n+-   `cl_fixture_cleanup(const char *)`: Tears down the previous fixture\n+    sandbox.\n+\n+-   `cl_fixture(const char *)`: Gets the full path to a fixture file.\n+\n+Please do note that these methods are *always* available whilst running a\n+test, even when calling auxiliary/static functions inside the same file.\n+\n+It's strongly encouraged to perform test assertions in auxiliary methods,\n+instead of returning error values. This is considered good Clar style.\n+\n+Style Example:\n+\n+~~~~ c\n+/*\n+ * Bad style: auxiliary functions return an error code\n+ */\n+\n+static int check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    if (aux == NULL)\n+        return -1;\n+\n+    return strcmp(my_function(aux), str) == 0 ? 0 : -1;\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    cl_must_pass_(\n+        check_string(\"foo\"),\n+        \"String differs after processing\"\n+    );\n+\n+    cl_must_pass_(\n+        check_string(\"bar\"),\n+        \"String differs after processing\"\n+    );\n+}\n+~~~~\n+\n+~~~~ c\n+/*\n+ * Good style: auxiliary functions perform assertions\n+ */\n+\n+static void check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    cl_assert_(\n+        aux != NULL,\n+        \"String processing failed\"\n+    );\n+\n+    cl_assert_(\n+        strcmp(my_function(aux), str) == 0,\n+        \"String differs after processing\"\n+    );\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    check_string(\"foo\");\n+    check_string(\"bar\");\n+}\n+~~~~\n+\n+About Clar\n+==========\n+\n+Clar has been written from scratch by [Vicent Martí](https://github.com/vmg),\n+to replace the old testing framework in [libgit2][libgit2].\n+\n+Do you know what languages are *in* on the SF startup scene? Node.js *and*\n+Latin.  Follow [@vmg](https://www.twitter.com/vmg) on Twitter to\n+receive more lessons on word etymology. You can be hip too.\n+\n+\n+[libgit2]: https://github.com/libgit2/libgit2\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nnew file mode 100644\nindex 0000000000..3fc2c76815\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.c\n@@ -0,0 +1,842 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#include <assert.h>\n+#include <setjmp.h>\n+#include <stdlib.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <math.h>\n+#include <stdarg.h>\n+#include <wchar.h>\n+#include <time.h>\n+\n+/* required for sandboxing */\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#ifdef _WIN32\n+#\tinclude <windows.h>\n+#\tinclude <io.h>\n+#\tinclude <shellapi.h>\n+#\tinclude <direct.h>\n+\n+#\tdefine _MAIN_CC __cdecl\n+\n+#\tifndef stat\n+#\t\tdefine stat(path, st) _stat(path, st)\n+#\tendif\n+#\tifndef mkdir\n+#\t\tdefine mkdir(path, mode) _mkdir(path)\n+#\tendif\n+#\tifndef chdir\n+#\t\tdefine chdir(path) _chdir(path)\n+#\tendif\n+#\tifndef access\n+#\t\tdefine access(path, mode) _access(path, mode)\n+#\tendif\n+#\tifndef strdup\n+#\t\tdefine strdup(str) _strdup(str)\n+#\tendif\n+#\tifndef strcasecmp\n+#\t\tdefine strcasecmp(a,b) _stricmp(a,b)\n+#\tendif\n+\n+#\tifndef __MINGW32__\n+#\t\tpragma comment(lib, \"shell32\")\n+#\t\tifndef strncpy\n+#\t\t\tdefine strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)\n+#\t\tendif\n+#\t\tifndef W_OK\n+#\t\t\tdefine W_OK 02\n+#\t\tendif\n+#\t\tifndef S_ISDIR\n+#\t\t\tdefine S_ISDIR(x) ((x & _S_IFDIR) != 0)\n+#\t\tendif\n+#\t\tdefine p_snprintf(buf,sz,fmt,...) _snprintf_s(buf,sz,_TRUNCATE,fmt,__VA_ARGS__)\n+#\telse\n+#\t\tdefine p_snprintf snprintf\n+#\tendif\n+\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"Iu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"Ix\"\n+#\tendif\n+\n+#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+\ttypedef struct stat STAT_T;\n+#\telse\n+\ttypedef struct _stat STAT_T;\n+#\tendif\n+#else\n+#\tinclude <sys/wait.h> /* waitpid(2) */\n+#\tinclude <unistd.h>\n+#\tdefine _MAIN_CC\n+#\tdefine p_snprintf snprintf\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"zu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"zx\"\n+#\tendif\n+\ttypedef struct stat STAT_T;\n+#endif\n+\n+#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n+\n+#include \"clar.h\"\n+\n+static void fs_rm(const char *_source);\n+static void fs_copy(const char *_source, const char *dest);\n+\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name);\n+#endif\n+\n+struct clar_error {\n+\tconst char *file;\n+\tconst char *function;\n+\tsize_t line_number;\n+\tconst char *error_msg;\n+\tchar *description;\n+\n+\tstruct clar_error *next;\n+};\n+\n+struct clar_explicit {\n+\tsize_t suite_idx;\n+\tconst char *filter;\n+\n+\tstruct clar_explicit *next;\n+};\n+\n+struct clar_report {\n+\tconst char *test;\n+\tint test_number;\n+\tconst char *suite;\n+\n+\tenum cl_test_status status;\n+\ttime_t start;\n+\tdouble elapsed;\n+\n+\tstruct clar_error *errors;\n+\tstruct clar_error *last_error;\n+\n+\tstruct clar_report *next;\n+};\n+\n+struct clar_summary {\n+\tconst char *filename;\n+\tFILE *fp;\n+};\n+\n+static struct {\n+\tenum cl_test_status test_status;\n+\n+\tconst char *active_test;\n+\tconst char *active_suite;\n+\n+\tint total_skipped;\n+\tint total_errors;\n+\n+\tint tests_ran;\n+\tint suites_ran;\n+\n+\tenum cl_output_format output_format;\n+\n+\tint report_errors_only;\n+\tint exit_on_error;\n+\tint verbosity;\n+\n+\tint write_summary;\n+\tchar *summary_filename;\n+\tstruct clar_summary *summary;\n+\n+\tstruct clar_explicit *explicit;\n+\tstruct clar_explicit *last_explicit;\n+\n+\tstruct clar_report *reports;\n+\tstruct clar_report *last_report;\n+\n+\tvoid (*local_cleanup)(void *);\n+\tvoid *local_cleanup_payload;\n+\n+\tjmp_buf trampoline;\n+\tint trampoline_enabled;\n+\n+\tcl_trace_cb *pfn_trace_cb;\n+\tvoid *trace_payload;\n+\n+} _clar;\n+\n+struct clar_func {\n+\tconst char *name;\n+\tvoid (*ptr)(void);\n+};\n+\n+struct clar_suite {\n+\tconst char *name;\n+\tstruct clar_func initialize;\n+\tstruct clar_func cleanup;\n+\tconst struct clar_func *tests;\n+\tsize_t test_count;\n+\tint enabled;\n+};\n+\n+/* From clar_print_*.c */\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n+static void clar_print_onsuite(const char *suite_name, int suite_index);\n+static void clar_print_onabort(const char *msg, ...);\n+\n+/* From clar_sandbox.c */\n+static void clar_unsandbox(void);\n+static int clar_sandbox(void);\n+\n+/* From summary.h */\n+static struct clar_summary *clar_summary_init(const char *filename);\n+static int clar_summary_shutdown(struct clar_summary *fp);\n+\n+/* Load the declarations for the test suite */\n+#include \"clar.suite\"\n+\n+\n+#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n+\t} while (0)\n+\n+void cl_trace_register(cl_trace_cb *cb, void *payload)\n+{\n+\t_clar.pfn_trace_cb = cb;\n+\t_clar.trace_payload = payload;\n+}\n+\n+\n+/* Core test functions */\n+static void\n+clar_report_errors(struct clar_report *report)\n+{\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (error = report->errors; error; error = error->next)\n+\t\tclar_print_error(i++, _clar.last_report, error);\n+}\n+\n+static void\n+clar_report_all(void)\n+{\n+\tstruct clar_report *report;\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (report = _clar.reports; report; report = report->next) {\n+\t\tif (report->status != CL_TEST_FAILURE)\n+\t\t\tcontinue;\n+\n+\t\tfor (error = report->errors; error; error = error->next)\n+\t\t\tclar_print_error(i++, report, error);\n+\t}\n+}\n+\n+#ifdef WIN32\n+# define clar_time DWORD\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\t*out = GetTickCount();\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)*end - (double)*start) / 1000;\n+}\n+#else\n+# include <sys/time.h>\n+\n+# define clar_time struct timeval\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\tstruct timezone tz;\n+\n+\tgettimeofday(out, &tz);\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n+\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n+}\n+#endif\n+\n+static void\n+clar_run_test(\n+\tconst struct clar_suite *suite,\n+\tconst struct clar_func *test,\n+\tconst struct clar_func *initialize,\n+\tconst struct clar_func *cleanup)\n+{\n+\tclar_time start, end;\n+\n+\t_clar.trampoline_enabled = 1;\n+\n+\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n+\n+\t_clar.last_report->start = time(NULL);\n+\tclar_time_now(&start);\n+\n+\tif (setjmp(_clar.trampoline) == 0) {\n+\t\tif (initialize->ptr != NULL)\n+\t\t\tinitialize->ptr();\n+\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n+\t\ttest->ptr();\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n+\t}\n+\n+\tclar_time_now(&end);\n+\n+\t_clar.trampoline_enabled = 0;\n+\n+\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n+\t\t_clar.last_report->status = CL_TEST_OK;\n+\n+\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n+\n+\tif (_clar.local_cleanup != NULL)\n+\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n+\n+\tif (cleanup->ptr != NULL)\n+\t\tcleanup->ptr();\n+\n+\tCL_TRACE(CL_TRACE__TEST__END);\n+\n+\t_clar.tests_ran++;\n+\n+\t/* remove any local-set cleanup methods */\n+\t_clar.local_cleanup = NULL;\n+\t_clar.local_cleanup_payload = NULL;\n+\n+\tif (_clar.report_errors_only) {\n+\t\tclar_report_errors(_clar.last_report);\n+\t} else {\n+\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n+\t}\n+}\n+\n+static void\n+clar_run_suite(const struct clar_suite *suite, const char *filter)\n+{\n+\tconst struct clar_func *test = suite->tests;\n+\tsize_t i, matchlen;\n+\tstruct clar_report *report;\n+\tint exact = 0;\n+\n+\tif (!suite->enabled)\n+\t\treturn;\n+\n+\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\treturn;\n+\n+\tif (!_clar.report_errors_only)\n+\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n+\n+\t_clar.active_suite = suite->name;\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n+\n+\tif (filter) {\n+\t\tsize_t suitelen = strlen(suite->name);\n+\t\tmatchlen = strlen(filter);\n+\t\tif (matchlen <= suitelen) {\n+\t\t\tfilter = NULL;\n+\t\t} else {\n+\t\t\tfilter += suitelen;\n+\t\t\twhile (*filter == ':')\n+\t\t\t\t++filter;\n+\t\t\tmatchlen = strlen(filter);\n+\n+\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n+\t\t\t\texact = 1;\n+\t\t\t\tmatchlen--;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (i = 0; i < suite->test_count; ++i) {\n+\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n+\t\t\tcontinue;\n+\n+\t\tif (exact && strlen(test[i].name) != matchlen)\n+\t\t\tcontinue;\n+\n+\t\t_clar.active_test = test[i].name;\n+\n+\t\treport = calloc(1, sizeof(struct clar_report));\n+\t\treport->suite = _clar.active_suite;\n+\t\treport->test = _clar.active_test;\n+\t\treport->test_number = _clar.tests_ran;\n+\t\treport->status = CL_TEST_NOTRUN;\n+\n+\t\tif (_clar.reports == NULL)\n+\t\t\t_clar.reports = report;\n+\n+\t\tif (_clar.last_report != NULL)\n+\t\t\t_clar.last_report->next = report;\n+\n+\t\t_clar.last_report = report;\n+\n+\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n+\n+\t\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\t\treturn;\n+\t}\n+\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_END);\n+}\n+\n+static void\n+clar_usage(const char *arg)\n+{\n+\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n+\tprintf(\"Options:\\n\");\n+\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n+\tprintf(\"  -iname        Include the suite with `name`\\n\");\n+\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n+\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n+\tprintf(\"  -q            Only report tests that had an error\\n\");\n+\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n+\tprintf(\"  -t            Display results in tap format\\n\");\n+\tprintf(\"  -l            Print suite names\\n\");\n+\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n+\texit(-1);\n+}\n+\n+static void\n+clar_parse_args(int argc, char **argv)\n+{\n+\tint i;\n+\n+\t/* Verify options before execute */\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tif (argument[0] != '-' || argument[1] == '\\0'\n+\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n+\t\t\tclar_usage(argv[0]);\n+\t\t}\n+\t}\n+\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tswitch (argument[1]) {\n+\t\tcase 's':\n+\t\tcase 'i':\n+\t\tcase 'x': { /* given suite name */\n+\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n+\t\t\tchar action = argument[1];\n+\t\t\tsize_t j, arglen, suitelen, cmplen;\n+\n+\t\t\targument += offset;\n+\t\t\targlen = strlen(argument);\n+\n+\t\t\tif (arglen == 0)\n+\t\t\t\tclar_usage(argv[0]);\n+\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n+\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n+\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n+\n+\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n+\t\t\t\t\tint exact = (arglen >= suitelen);\n+\n+\t\t\t\t\t/* Do we have a real suite prefix separated by a\n+\t\t\t\t\t * trailing '::' or just a matching substring? */\n+\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n+\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n+\t\t\t\t\t    continue;\n+\n+\t\t\t\t\t++found;\n+\n+\t\t\t\t\tif (!exact)\n+\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n+\n+\t\t\t\t\tswitch (action) {\n+\t\t\t\t\tcase 's': {\n+\t\t\t\t\t\tstruct clar_explicit *explicit =\n+\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n+\t\t\t\t\t\tassert(explicit);\n+\n+\t\t\t\t\t\texplicit->suite_idx = j;\n+\t\t\t\t\t\texplicit->filter = argument;\n+\n+\t\t\t\t\t\tif (_clar.explicit == NULL)\n+\t\t\t\t\t\t\t_clar.explicit = explicit;\n+\n+\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n+\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n+\n+\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n+\t\t\t\t\t\t_clar.last_explicit = explicit;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n+\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif (exact)\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (!found) {\n+\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n+\t\t\t\texit(-1);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase 'q':\n+\t\t\t_clar.report_errors_only = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 'Q':\n+\t\t\t_clar.exit_on_error = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 't':\n+\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n+\t\t\tbreak;\n+\n+\t\tcase 'l': {\n+\t\t\tsize_t j;\n+\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n+\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n+\n+\t\t\texit(0);\n+\t\t}\n+\n+\t\tcase 'v':\n+\t\t\t_clar.verbosity++;\n+\t\t\tbreak;\n+\n+\t\tcase 'r':\n+\t\t\t_clar.write_summary = 1;\n+\t\t\tfree(_clar.summary_filename);\n+\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tassert(!\"Unexpected commandline argument!\");\n+\t\t}\n+\t}\n+}\n+\n+void\n+clar_test_init(int argc, char **argv)\n+{\n+\tconst char *summary_env;\n+\n+\tif (argc > 1)\n+\t\tclar_parse_args(argc, argv);\n+\n+\tclar_print_init(\n+\t\t(int)_clar_callback_count,\n+\t\t(int)_clar_suite_count,\n+\t\t\"\"\n+\t);\n+\n+\tif (!_clar.summary_filename &&\n+\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n+\t\t_clar.write_summary = 1;\n+\t\t_clar.summary_filename = strdup(summary_env);\n+\t}\n+\n+\tif (_clar.write_summary && !_clar.summary_filename)\n+\t\t_clar.summary_filename = strdup(\"summary.xml\");\n+\n+\tif (_clar.write_summary &&\n+\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n+\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tif (clar_sandbox() < 0) {\n+\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n+\t\texit(-1);\n+\t}\n+}\n+\n+int\n+clar_test_run(void)\n+{\n+\tsize_t i;\n+\tstruct clar_explicit *explicit;\n+\n+\tif (_clar.explicit) {\n+\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n+\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n+\t} else {\n+\t\tfor (i = 0; i < _clar_suite_count; ++i)\n+\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n+\t}\n+\n+\treturn _clar.total_errors;\n+}\n+\n+void\n+clar_test_shutdown(void)\n+{\n+\tstruct clar_explicit *explicit, *explicit_next;\n+\tstruct clar_report *report, *report_next;\n+\n+\tclar_print_shutdown(\n+\t\t_clar.tests_ran,\n+\t\t(int)_clar_suite_count,\n+\t\t_clar.total_errors\n+\t);\n+\n+\tclar_unsandbox();\n+\n+\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n+\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n+\t\texplicit_next = explicit->next;\n+\t\tfree(explicit);\n+\t}\n+\n+\tfor (report = _clar.reports; report; report = report_next) {\n+\t\treport_next = report->next;\n+\t\tfree(report);\n+\t}\n+\n+\tfree(_clar.summary_filename);\n+}\n+\n+int\n+clar_test(int argc, char **argv)\n+{\n+\tint errors;\n+\n+\tclar_test_init(argc, argv);\n+\terrors = clar_test_run();\n+\tclar_test_shutdown();\n+\n+\treturn errors;\n+}\n+\n+static void abort_test(void)\n+{\n+\tif (!_clar.trampoline_enabled) {\n+\t\tclar_print_onabort(\n+\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n+\t\tclar_report_errors(_clar.last_report);\n+\t\texit(-1);\n+\t}\n+\n+\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n+\tlongjmp(_clar.trampoline, -1);\n+}\n+\n+void clar__skip(void)\n+{\n+\t_clar.last_report->status = CL_TEST_SKIP;\n+\t_clar.total_skipped++;\n+\tabort_test();\n+}\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n+\n+\tif (_clar.last_report->errors == NULL)\n+\t\t_clar.last_report->errors = error;\n+\n+\tif (_clar.last_report->last_error != NULL)\n+\t\t_clar.last_report->last_error->next = error;\n+\n+\t_clar.last_report->last_error = error;\n+\n+\terror->file = file;\n+\terror->function = function;\n+\terror->line_number = line;\n+\terror->error_msg = error_msg;\n+\n+\tif (description != NULL)\n+\t\terror->description = strdup(description);\n+\n+\t_clar.total_errors++;\n+\t_clar.last_report->status = CL_TEST_FAILURE;\n+\n+\tif (should_abort)\n+\t\tabort_test();\n+}\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tif (condition)\n+\t\treturn;\n+\n+\tclar__fail(file, function, line, error_msg, description, should_abort);\n+}\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...)\n+{\n+\tva_list args;\n+\tchar buf[4096];\n+\tint is_equal = 1;\n+\n+\tva_start(args, fmt);\n+\n+\tif (!strcmp(\"%s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n+\t\t\t\t\ts1, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n+\t\t\t\t\tlen, s1, len, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n+\t\t\t\t\twcs1, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n+\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n+\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n+\t\tis_equal = (sz1 == sz2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%p\", fmt)) {\n+\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n+\t\tis_equal = (p1 == p2);\n+\t\tif (!is_equal)\n+\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n+\t}\n+\telse {\n+\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n+\t\tis_equal = (i1 == i2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n+\t\t}\n+\t}\n+\n+\tva_end(args);\n+\n+\tif (!is_equal)\n+\t\tclar__fail(file, function, line, err, buf, should_abort);\n+}\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n+{\n+\t_clar.local_cleanup = cleanup;\n+\t_clar.local_cleanup_payload = opaque;\n+}\n+\n+#include \"clar/sandbox.h\"\n+#include \"clar/fixtures.h\"\n+#include \"clar/fs.h\"\n+#include \"clar/print.h\"\n+#include \"clar/summary.h\"\ndiff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\nnew file mode 100644\nindex 0000000000..8c22382bd5\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.h\n@@ -0,0 +1,173 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST_H__\n+#define __CLAR_TEST_H__\n+\n+#include <stdlib.h>\n+\n+enum cl_test_status {\n+\tCL_TEST_OK,\n+\tCL_TEST_FAILURE,\n+\tCL_TEST_SKIP,\n+\tCL_TEST_NOTRUN,\n+};\n+\n+enum cl_output_format {\n+\tCL_OUTPUT_CLAP,\n+\tCL_OUTPUT_TAP,\n+};\n+\n+/** Setup clar environment */\n+void clar_test_init(int argc, char *argv[]);\n+int clar_test_run(void);\n+void clar_test_shutdown(void);\n+\n+/** One shot setup & run */\n+int clar_test(int argc, char *argv[]);\n+\n+const char *clar_sandbox_path(void);\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n+void cl_fs_cleanup(void);\n+\n+/**\n+ * cl_trace_* is a hook to provide a simple global tracing\n+ * mechanism.\n+ *\n+ * The goal here is to let main() provide clar-proper\n+ * with a callback to optionally write log info for\n+ * test operations into the same stream used by their\n+ * actual tests.  This would let them print test names\n+ * and maybe performance data as they choose.\n+ *\n+ * The goal is NOT to alter the flow of control or to\n+ * override test selection/skipping.  (So the callback\n+ * does not return a value.)\n+ *\n+ * The goal is NOT to duplicate the existing\n+ * pass/fail/skip reporting.  (So the callback\n+ * does not accept a status/errorcode argument.)\n+ *\n+ */\n+typedef enum cl_trace_event {\n+\tCL_TRACE__SUITE_BEGIN,\n+\tCL_TRACE__SUITE_END,\n+\tCL_TRACE__TEST__BEGIN,\n+\tCL_TRACE__TEST__END,\n+\tCL_TRACE__TEST__RUN_BEGIN,\n+\tCL_TRACE__TEST__RUN_END,\n+\tCL_TRACE__TEST__LONGJMP,\n+} cl_trace_event;\n+\n+typedef void (cl_trace_cb)(\n+\tcl_trace_event ev,\n+\tconst char *suite_name,\n+\tconst char *test_name,\n+\tvoid *payload);\n+\n+/**\n+ * Register a callback into CLAR to send global trace events.\n+ * Pass NULL to disable.\n+ */\n+void cl_trace_register(cl_trace_cb *cb, void *payload);\n+\n+\n+#ifdef CLAR_FIXTURE_PATH\n+const char *cl_fixture(const char *fixture_name);\n+void cl_fixture_sandbox(const char *fixture_name);\n+void cl_fixture_cleanup(const char *fixture_name);\n+const char *cl_fixture_basename(const char *fixture_name);\n+#endif\n+\n+/**\n+ * Assertion macros with explicit error message\n+ */\n+#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n+#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n+#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n+\n+/**\n+ * Check macros with explicit error message\n+ */\n+#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n+#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n+#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n+\n+/**\n+ * Assertion macros with no error message\n+ */\n+#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n+#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n+#define cl_assert(expr) cl_assert_(expr, NULL)\n+\n+/**\n+ * Check macros with no error message\n+ */\n+#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n+#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n+#define cl_check(expr) cl_check_(expr, NULL)\n+\n+/**\n+ * Forced failure/warning\n+ */\n+#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n+#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n+\n+#define cl_skip() clar__skip()\n+\n+/**\n+ * Typed assertion macros\n+ */\n+#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n+#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n+\n+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n+\n+#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n+#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n+\n+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+\n+#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n+#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n+#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n+\n+#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n+\n+#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n+\n+void clar__skip(void);\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...);\n+\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\nnew file mode 100644\nindex 0000000000..6ec6423484\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fixtures.h\n@@ -0,0 +1,50 @@\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name)\n+{\n+\tstatic char _path[4096];\n+\tsize_t root_len;\n+\n+\troot_len = strlen(base);\n+\tstrncpy(_path, base, sizeof(_path));\n+\n+\tif (_path[root_len - 1] != '/')\n+\t\t_path[root_len++] = '/';\n+\n+\tif (fixture_name[0] == '/')\n+\t\tfixture_name++;\n+\n+\tstrncpy(_path + root_len,\n+\t\tfixture_name,\n+\t\tsizeof(_path) - root_len);\n+\n+\treturn _path;\n+}\n+\n+const char *cl_fixture(const char *fixture_name)\n+{\n+\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n+}\n+\n+void cl_fixture_sandbox(const char *fixture_name)\n+{\n+\tfs_copy(cl_fixture(fixture_name), _clar_path);\n+}\n+\n+const char *cl_fixture_basename(const char *fixture_name)\n+{\n+\tconst char *p;\n+\n+\tfor (p = fixture_name; *p; p++) {\n+\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n+\t\t\tfixture_name = p+1;\n+\t}\n+\n+\treturn fixture_name;\n+}\n+\n+void cl_fixture_cleanup(const char *fixture_name)\n+{\n+\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nnew file mode 100644\nindex 0000000000..a6eda5e5dc\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -0,0 +1,522 @@\n+/*\n+ * By default, use a read/write loop to copy files on POSIX systems.\n+ * On Linux, use sendfile by default as it's slightly faster.  On\n+ * macOS, we avoid fcopyfile by default because it's slightly slower.\n+ */\n+#undef USE_FCOPYFILE\n+#define USE_SENDFILE 1\n+\n+#ifdef _WIN32\n+\n+#ifdef CLAR_WIN32_LONGPATHS\n+# define CLAR_MAX_PATH 4096\n+#else\n+# define CLAR_MAX_PATH MAX_PATH\n+#endif\n+\n+#define RM_RETRY_COUNT\t5\n+#define RM_RETRY_DELAY\t10\n+\n+#ifdef __MINGW32__\n+\n+/* These security-enhanced functions are not available\n+ * in MinGW, so just use the vanilla ones */\n+#define wcscpy_s(a, b, c) wcscpy((a), (c))\n+#define wcscat_s(a, b, c) wcscat((a), (c))\n+\n+#endif /* __MINGW32__ */\n+\n+static int\n+fs__dotordotdot(WCHAR *_tocheck)\n+{\n+\treturn _tocheck[0] == '.' &&\n+\t\t(_tocheck[1] == '\\0' ||\n+\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n+}\n+\n+static int\n+fs_rmdir_rmdir(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\n+\twhile (!RemoveDirectoryW(_wpath)) {\n+\t\t/* Only retry when we have retries remaining, and the\n+\t\t * error was ERROR_DIR_NOT_EMPTY. */\n+\t\tif (retries++ > RM_RETRY_COUNT ||\n+\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n+\t\t\treturn -1;\n+\n+\t\t/* Give whatever has a handle to a child item some time\n+\t\t * to release it before trying again */\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void translate_path(WCHAR *path, size_t path_size)\n+{\n+    size_t path_len, i;\n+\n+    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n+\treturn;\n+\n+    path_len = wcslen(path);\n+    cl_assert(path_size > path_len + 4);\n+\n+    for (i = path_len; i > 0; i--) {\n+\tWCHAR c = path[i - 1];\n+\n+\tif (c == L'/')\n+\t    path[i + 3] = L'\\\\';\n+\telse\n+\t    path[i + 3] = path[i - 1];\n+    }\n+\n+    path[0] = L'\\\\';\n+    path[1] = L'\\\\';\n+    path[2] = L'?';\n+    path[3] = L'\\\\';\n+    path[path_len + 4] = L'\\0';\n+}\n+\n+static void\n+fs_rmdir_helper(WCHAR *_wsource)\n+{\n+\tWCHAR buffer[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buffer_prefix_len;\n+\n+\t/* Set up the buffer and capture the length */\n+\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n+\ttranslate_path(buffer, CLAR_MAX_PATH);\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n+\tbuffer_prefix_len = wcslen(buffer);\n+\n+\t/* FindFirstFile needs a wildcard to match multiple items */\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buffer, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_rmdir_helper(buffer);\n+\t\telse {\n+\t\t\t/* If set, the +R bit must be cleared before deleting */\n+\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n+\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\t\tcl_assert(DeleteFileW(buffer));\n+\t\t}\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+\n+\t/* Now that the directory is empty, remove it */\n+\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n+}\n+\n+static int\n+fs_rm_wait(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\tDWORD last_error;\n+\n+\tdo {\n+\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n+\t\t\tlast_error = GetLastError();\n+\t\telse\n+\t\t\tlast_error = ERROR_SUCCESS;\n+\n+\t\t/* Is the item gone? */\n+\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n+\t\t\tERROR_PATH_NOT_FOUND == last_error)\n+\t\t\treturn 0;\n+\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n+\t}\n+\twhile (retries++ <= RM_RETRY_COUNT);\n+\n+\treturn -1;\n+}\n+\n+static void\n+fs_rm(const char *_source)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH];\n+\tDWORD attrs;\n+\n+\t/* The input path is UTF-8. Convert it to wide characters\n+\t * for use with the Windows API */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1, /* Indicates NULL termination */\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\n+\t/* Does the item exist? If not, we have no work to do */\n+\tattrs = GetFileAttributesW(wsource);\n+\n+\tif (INVALID_FILE_ATTRIBUTES == attrs)\n+\t\treturn;\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n+\t\tfs_rmdir_helper(wsource);\n+\telse {\n+\t\t/* The item is a file. Strip the +R bit */\n+\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n+\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\tcl_assert(DeleteFileW(wsource));\n+\t}\n+\n+\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n+\tcl_assert(0 == fs_rm_wait(wsource));\n+}\n+\n+static void\n+fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n+{\n+\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n+\n+\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_source, CLAR_MAX_PATH);\n+\tbuf_source_prefix_len = wcslen(buf_source);\n+\n+\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n+\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n+\tbuf_dest_prefix_len = wcslen(buf_dest);\n+\n+\t/* Get an enumerator for the items in the source. */\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buf_source, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\t/* Create the target directory. */\n+\tcl_assert(CreateDirectoryW(_wdest, NULL));\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n+\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_copydir_helper(buf_source, buf_dest);\n+\t\telse\n+\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+}\n+\n+static void\n+fs_copy(const char *_source, const char *_dest)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n+\tDWORD source_attrs, dest_attrs;\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\n+\t/* The input paths are UTF-8. Convert them to wide characters\n+\t * for use with the Windows API. */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1,\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_dest,\n+\t\t\t\t-1,\n+\t\t\t\twdest,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\ttranslate_path(wdest, CLAR_MAX_PATH);\n+\n+\t/* Check the source for existence */\n+\tsource_attrs = GetFileAttributesW(wsource);\n+\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n+\n+\t/* Check the target for existence */\n+\tdest_attrs = GetFileAttributesW(wdest);\n+\n+\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n+\t\t/* Target exists; append last path part of source to target.\n+\t\t * Use FindFirstFile to parse the path */\n+\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n+\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n+\t\tFindClose(find_handle);\n+\n+\t\t/* Check the new target for existence */\n+\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n+\t}\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n+\t\tfs_copydir_helper(wsource, wdest);\n+\telse\n+\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+#ifdef CLAR_FIXTURE_PATH\n+\tfs_rm(fixture_path(_clar_path, \"*\"));\n+#endif\n+}\n+\n+#else\n+\n+#include <errno.h>\n+#include <string.h>\n+#include <limits.h>\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#if defined(__linux__)\n+# include <sys/sendfile.h>\n+#endif\n+\n+#if defined(__APPLE__)\n+# include <copyfile.h>\n+#endif\n+\n+static void basename_r(const char **out, int *out_len, const char *in)\n+{\n+\tsize_t in_len = strlen(in), start_pos;\n+\n+\tfor (in_len = strlen(in); in_len; in_len--) {\n+\t\tif (in[in_len - 1] != '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tfor (start_pos = in_len; start_pos; start_pos--) {\n+\t\tif (in[start_pos - 1] == '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tcl_assert(in_len - start_pos < INT_MAX);\n+\n+\tif (in_len - start_pos > 0) {\n+\t\t*out = &in[start_pos];\n+\t\t*out_len = (in_len - start_pos);\n+\t} else {\n+\t\t*out = \"/\";\n+\t\t*out_len = 1;\n+\t}\n+}\n+\n+static char *joinpath(const char *dir, const char *base, int base_len)\n+{\n+\tchar *out;\n+\tint len;\n+\n+\tif (base_len == -1) {\n+\t\tsize_t bl = strlen(base);\n+\n+\t\tcl_assert(bl < INT_MAX);\n+\t\tbase_len = (int)bl;\n+\t}\n+\n+\tlen = strlen(dir) + base_len + 2;\n+\tcl_assert(len > 0);\n+\n+\tcl_assert(out = malloc(len));\n+\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n+\n+\treturn out;\n+}\n+\n+static void\n+fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n+{\n+\tDIR *source_dir;\n+\tstruct dirent *d;\n+\n+\tmkdir(dest, dest_mode);\n+\n+\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n+\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(source, d->d_name, -1);\n+\t\tfs_copy(child, dest);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\n+\tclosedir(source_dir);\n+}\n+\n+static void\n+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n+{\n+\tint in, out;\n+\n+\tcl_must_pass((in = open(source, O_RDONLY)));\n+\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n+\n+#if USE_FCOPYFILE && defined(__APPLE__)\n+\t((void)(source_len)); /* unused */\n+\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n+#elif USE_SENDFILE && defined(__linux__)\n+\t{\n+\t\tssize_t ret = 0;\n+\n+\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n+\t\t\tsource_len -= (size_t)ret;\n+\t\t}\n+\t\tcl_assert(ret >= 0);\n+\t}\n+#else\n+\t{\n+\t\tchar buf[131072];\n+\t\tssize_t ret;\n+\n+\t\t((void)(source_len)); /* unused */\n+\n+\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n+\t\t\tsize_t len = (size_t)ret;\n+\n+\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n+\t\t\t\tcl_assert(ret <= (ssize_t)len);\n+\t\t\t\tlen -= ret;\n+\t\t\t}\n+\t\t\tcl_assert(ret >= 0);\n+\t\t}\n+\t\tcl_assert(ret == 0);\n+\t}\n+#endif\n+\n+\tclose(in);\n+\tclose(out);\n+}\n+\n+static void\n+fs_copy(const char *source, const char *_dest)\n+{\n+\tchar *dbuf = NULL;\n+\tconst char *dest = NULL;\n+\tstruct stat source_st, dest_st;\n+\n+\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n+\n+\tif (lstat(_dest, &dest_st) == 0) {\n+\t\tconst char *base;\n+\t\tint base_len;\n+\n+\t\t/* Target exists and is directory; append basename */\n+\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n+\n+\t\tbasename_r(&base, &base_len, source);\n+\t\tcl_assert(base_len < INT_MAX);\n+\n+\t\tdbuf = joinpath(_dest, base, base_len);\n+\t\tdest = dbuf;\n+\t} else if (errno != ENOENT) {\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t} else {\n+\t\tdest = _dest;\n+\t}\n+\n+\tif (S_ISDIR(source_st.st_mode)) {\n+\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n+\t} else {\n+\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n+\t}\n+\n+\tfree(dbuf);\n+}\n+\n+static void\n+fs_rmdir_helper(const char *path)\n+{\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n+\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(path, d->d_name, -1);\n+\t\tfs_rm(child);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\tclosedir(dir);\n+\n+\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n+}\n+\n+static void\n+fs_rm(const char *path)\n+{\n+\tstruct stat st;\n+\n+\tif (lstat(path, &st)) {\n+\t\tif (errno == ENOENT)\n+\t\t\treturn;\n+\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t}\n+\n+\tif (S_ISDIR(st.st_mode)) {\n+\t\tfs_rmdir_helper(path);\n+\t} else {\n+\t\tcl_must_pass(unlink(path));\n+\t}\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+\tclar_unsandbox();\n+\tclar_sandbox();\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\nnew file mode 100644\nindex 0000000000..c17e2f693b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/print.h\n@@ -0,0 +1,211 @@\n+/* clap: clar protocol, the traditional clar output format */\n+\n+static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n+\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n+}\n+\n+static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"\\n\\n\");\n+\tclar_report_all();\n+}\n+\n+static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tprintf(\"  %d) Failure:\\n\", num);\n+\n+\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n+\t\treport->suite,\n+\t\treport->test,\n+\t\terror->file,\n+\t\terror->line_number);\n+\n+\tprintf(\"  %s\\n\", error->error_msg);\n+\n+\tif (error->description != NULL)\n+\t\tprintf(\"  %s\\n\", error->description);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tif (_clar.verbosity > 1) {\n+\t\tprintf(\"%s::%s: \", suite_name, test_name);\n+\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n+\t\t}\n+\t} else {\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\".\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n+\t\t}\n+\n+\t\tfflush(stdout);\n+\t}\n+}\n+\n+static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tif (_clar.verbosity == 1)\n+\t\tprintf(\"\\n%s\", suite_name);\n+\n+\t(void)suite_index;\n+}\n+\n+static void clar_print_clap_onabort(const char *fmt, va_list arg)\n+{\n+\tvfprintf(stderr, fmt, arg);\n+}\n+\n+/* tap: test anywhere protocol format */\n+\n+static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)suite_names;\n+\tprintf(\"TAP version 13\\n\");\n+}\n+\n+static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"1..%d\\n\", test_count);\n+}\n+\n+static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\t(void)num;\n+\t(void)report;\n+\t(void)error;\n+}\n+\n+static void print_escaped(const char *str)\n+{\n+\tchar *c;\n+\n+\twhile ((c = strchr(str, '\\'')) != NULL) {\n+\t\tprintf(\"%.*s\", (int)(c - str), str);\n+\t\tprintf(\"''\");\n+\t\tstr = c + 1;\n+\t}\n+\n+\tprintf(\"%s\", str);\n+}\n+\n+static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tconst struct clar_error *error = _clar.last_report->errors;\n+\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tswitch(status) {\n+\tcase CL_TEST_OK:\n+\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\tcase CL_TEST_FAILURE:\n+\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\n+\t\tprintf(\"    ---\\n\");\n+\t\tprintf(\"    reason: |\\n\");\n+\t\tprintf(\"      %s\\n\", error->error_msg);\n+\n+\t\tif (error->description)\n+\t\t\tprintf(\"      %s\\n\", error->description);\n+\n+\t\tprintf(\"    at:\\n\");\n+\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n+\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n+\t\tprintf(\"      function: '%s'\\n\", error->function);\n+\t\tprintf(\"    ---\\n\");\n+\n+\t\tbreak;\n+\tcase CL_TEST_SKIP:\n+\tcase CL_TEST_NOTRUN:\n+\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\t}\n+\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n+}\n+\n+static void clar_print_tap_onabort(const char *fmt, va_list arg)\n+{\n+\tprintf(\"Bail out! \");\n+\tvprintf(fmt, arg);\n+\tfflush(stdout);\n+}\n+\n+/* indirection between protocol output selection */\n+\n+#define PRINT(FN, ...) do { \\\n+\t\tswitch (_clar.output_format) { \\\n+\t\t\tcase CL_OUTPUT_CLAP: \\\n+\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tcase CL_OUTPUT_TAP: \\\n+\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tdefault: \\\n+\t\t\t\tabort(); \\\n+\t\t} \\\n+\t} while (0)\n+\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\tPRINT(init, test_count, suite_count, suite_names);\n+}\n+\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\tPRINT(shutdown, test_count, suite_count, error_count);\n+}\n+\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tPRINT(error, num, report, error);\n+}\n+\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tPRINT(ontest, suite_name, test_name, test_number, status);\n+}\n+\n+static void clar_print_onsuite(const char *suite_name, int suite_index)\n+{\n+\tPRINT(onsuite, suite_name, suite_index);\n+}\n+\n+static void clar_print_onabort(const char *msg, ...)\n+{\n+\tva_list argp;\n+\tva_start(argp, msg);\n+\tPRINT(onabort, msg, argp);\n+\tva_end(argp);\n+}\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nnew file mode 100644\nindex 0000000000..0ba1479620\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -0,0 +1,154 @@\n+#ifdef __APPLE__\n+#include <sys/syslimits.h>\n+#endif\n+\n+static char _clar_path[4096 + 1];\n+\n+static int\n+is_valid_tmp_path(const char *path)\n+{\n+\tSTAT_T st;\n+\n+\tif (stat(path, &st) != 0)\n+\t\treturn 0;\n+\n+\tif (!S_ISDIR(st.st_mode))\n+\t\treturn 0;\n+\n+\treturn (access(path, W_OK) == 0);\n+}\n+\n+static int\n+find_tmp_path(char *buffer, size_t length)\n+{\n+#ifndef _WIN32\n+\tstatic const size_t var_count = 5;\n+\tstatic const char *env_vars[] = {\n+\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n+ \t};\n+\n+ \tsize_t i;\n+\n+\tfor (i = 0; i < var_count; ++i) {\n+\t\tconst char *env = getenv(env_vars[i]);\n+\t\tif (!env)\n+\t\t\tcontinue;\n+\n+\t\tif (is_valid_tmp_path(env)) {\n+#ifdef __APPLE__\n+\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n+\t\t\t\treturn 0;\n+#endif\n+\t\t\tstrncpy(buffer, env, length - 1);\n+\t\t\tbuffer[length - 1] = '\\0';\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\t/* If the environment doesn't say anything, try to use /tmp */\n+\tif (is_valid_tmp_path(\"/tmp\")) {\n+#ifdef __APPLE__\n+\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n+\t\t\treturn 0;\n+#endif\n+\t\tstrncpy(buffer, \"/tmp\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+#else\n+\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n+\tif (env_len > 0 && env_len < (DWORD)length)\n+\t\treturn 0;\n+\n+\tif (GetTempPath((DWORD)length, buffer))\n+\t\treturn 0;\n+#endif\n+\n+\t/* This system doesn't like us, try to use the current directory */\n+\tif (is_valid_tmp_path(\".\")) {\n+\t\tstrncpy(buffer, \".\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static void clar_unsandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0')\n+\t\treturn;\n+\n+\tcl_must_pass(chdir(\"..\"));\n+\n+\tfs_rm(_clar_path);\n+}\n+\n+static int build_sandbox_path(void)\n+{\n+#ifdef CLAR_TMPDIR\n+\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n+#else\n+\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n+#endif\n+\n+\tsize_t len;\n+\n+\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n+\t\treturn -1;\n+\n+\tlen = strlen(_clar_path);\n+\n+#ifdef _WIN32\n+\t{ /* normalize path to POSIX forward slashes */\n+\t\tsize_t i;\n+\t\tfor (i = 0; i < len; ++i) {\n+\t\t\tif (_clar_path[i] == '\\\\')\n+\t\t\t\t_clar_path[i] = '/';\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (_clar_path[len - 1] != '/') {\n+\t\t_clar_path[len++] = '/';\n+\t}\n+\n+\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n+\n+#if defined(__MINGW32__)\n+\tif (_mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(_WIN32)\n+\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#else\n+\tif (mkdtemp(_clar_path) == NULL)\n+\t\treturn -1;\n+#endif\n+\n+\treturn 0;\n+}\n+\n+static int clar_sandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n+\t\treturn -1;\n+\n+\tif (chdir(_clar_path) != 0)\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const char *clar_sandbox_path(void)\n+{\n+\treturn _clar_path;\n+}\n+\ndiff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\nnew file mode 100644\nindex 0000000000..4dd352e28b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/summary.h\n@@ -0,0 +1,143 @@\n+\n+#include <stdio.h>\n+#include <time.h>\n+\n+static int clar_summary_close_tag(\n+    struct clar_summary *summary, const char *tag, int indent)\n+{\n+\tconst char *indt;\n+\n+\tif (indent == 0) indt = \"\";\n+\telse if (indent == 1) indt = \"\\t\";\n+\telse indt = \"\\t\\t\";\n+\n+\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n+}\n+\n+static int clar_summary_testsuites(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n+}\n+\n+static int clar_summary_testsuite(struct clar_summary *summary,\n+    int idn, const char *name, time_t timestamp,\n+    int test_count, int fail_count, int error_count)\n+{\n+\tstruct tm *tm = localtime(&timestamp);\n+\tchar iso_dt[20];\n+\n+\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n+\t\treturn -1;\n+\n+\treturn fprintf(summary->fp, \"\\t<testsuite\"\n+\t\t       \" id=\\\"%d\\\"\"\n+\t\t       \" name=\\\"%s\\\"\"\n+\t\t       \" hostname=\\\"localhost\\\"\"\n+\t\t       \" timestamp=\\\"%s\\\"\"\n+\t\t       \" tests=\\\"%d\\\"\"\n+\t\t       \" failures=\\\"%d\\\"\"\n+\t\t       \" errors=\\\"%d\\\">\\n\",\n+\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n+}\n+\n+static int clar_summary_testcase(struct clar_summary *summary,\n+    const char *name, const char *classname, double elapsed)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n+\t\tname, classname, elapsed);\n+}\n+\n+static int clar_summary_failure(struct clar_summary *summary,\n+    const char *type, const char *message, const char *desc)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n+\t    type, message, desc);\n+}\n+\n+static int clar_summary_skipped(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n+}\n+\n+struct clar_summary *clar_summary_init(const char *filename)\n+{\n+\tstruct clar_summary *summary;\n+\tFILE *fp;\n+\n+\tif ((fp = fopen(filename, \"w\")) == NULL) {\n+\t\tperror(\"fopen\");\n+\t\treturn NULL;\n+\t}\n+\n+\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n+\t\tperror(\"malloc\");\n+\t\tfclose(fp);\n+\t\treturn NULL;\n+\t}\n+\n+\tsummary->filename = filename;\n+\tsummary->fp = fp;\n+\n+\treturn summary;\n+}\n+\n+int clar_summary_shutdown(struct clar_summary *summary)\n+{\n+\tstruct clar_report *report;\n+\tconst char *last_suite = NULL;\n+\n+\tif (clar_summary_testsuites(summary) < 0)\n+\t\tgoto on_error;\n+\n+\treport = _clar.reports;\n+\twhile (report != NULL) {\n+\t\tstruct clar_error *error = report->errors;\n+\n+\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n+\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\n+\t\tlast_suite = report->suite;\n+\n+\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n+\n+\t\twhile (error != NULL) {\n+\t\t\tif (clar_summary_failure(summary, \"assert\",\n+\t\t\t    error->error_msg, error->description) < 0)\n+\t\t\t\tgoto on_error;\n+\n+\t\t\terror = error->next;\n+\t\t}\n+\n+\t\tif (report->status == CL_TEST_SKIP)\n+\t\t\tclar_summary_skipped(summary);\n+\n+\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n+\t\t\tgoto on_error;\n+\n+\t\treport = report->next;\n+\n+\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\t}\n+\n+\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n+\t    fclose(summary->fp) != 0)\n+\t\tgoto on_error;\n+\n+\tprintf(\"written summary file to %s\\n\", summary->filename);\n+\n+\tfree(summary);\n+\treturn 0;\n+\n+on_error:\n+\tfclose(summary->fp);\n+\tfree(summary);\n+\treturn -1;\n+}\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nnew file mode 100755\nindex 0000000000..931b4d60d9\n--- /dev/null\n+++ b/t/unit-tests/clar/generate.py\n@@ -0,0 +1,267 @@\n+#!/usr/bin/env python\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+from __future__ import with_statement\n+from string import Template\n+import re, fnmatch, os, sys, codecs, pickle\n+\n+class Module(object):\n+    class Template(object):\n+        def __init__(self, module):\n+            self.module = module\n+\n+        def _render_callback(self, cb):\n+            if not cb:\n+                return '    { NULL, NULL }'\n+            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n+\n+    class DeclarationTemplate(Template):\n+        def render(self):\n+            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n+\n+            for initializer in self.module.initializers:\n+                out += \"extern %s;\\n\" % initializer['declaration']\n+\n+            if self.module.cleanup:\n+                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n+\n+            return out\n+\n+    class CallbacksTemplate(Template):\n+        def render(self):\n+            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n+            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n+            out += \"\\n};\\n\"\n+            return out\n+\n+    class InfoTemplate(Template):\n+        def render(self):\n+            templates = []\n+\n+            initializers = self.module.initializers\n+            if len(initializers) == 0:\n+                initializers = [ None ]\n+\n+            for initializer in initializers:\n+                name = self.module.clean_name()\n+                if initializer and initializer['short_name'].startswith('initialize_'):\n+                    variant = initializer['short_name'][len('initialize_'):]\n+                    name += \" (%s)\" % variant.replace('_', ' ')\n+\n+                template = Template(\n+            r\"\"\"\n+    {\n+        \"${clean_name}\",\n+    ${initialize},\n+    ${cleanup},\n+        ${cb_ptr}, ${cb_count}, ${enabled}\n+    }\"\"\"\n+                ).substitute(\n+                    clean_name = name,\n+                    initialize = self._render_callback(initializer),\n+                    cleanup = self._render_callback(self.module.cleanup),\n+                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n+                    cb_count = len(self.module.callbacks),\n+                    enabled = int(self.module.enabled)\n+                )\n+                templates.append(template)\n+\n+            return ','.join(templates)\n+\n+    def __init__(self, name):\n+        self.name = name\n+\n+        self.mtime = None\n+        self.enabled = True\n+        self.modified = False\n+\n+    def clean_name(self):\n+        return self.name.replace(\"_\", \"::\")\n+\n+    def _skip_comments(self, text):\n+        SKIP_COMMENTS_REGEX = re.compile(\n+            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n+            re.DOTALL | re.MULTILINE)\n+\n+        def _replacer(match):\n+            s = match.group(0)\n+            return \"\" if s.startswith('/') else s\n+\n+        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n+\n+    def parse(self, contents):\n+        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n+\n+        contents = self._skip_comments(contents)\n+        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n+\n+        self.callbacks = []\n+        self.initializers = []\n+        self.cleanup = None\n+\n+        for (declaration, symbol, short_name) in regex.findall(contents):\n+            data = {\n+                \"short_name\" : short_name,\n+                \"declaration\" : declaration,\n+                \"symbol\" : symbol\n+            }\n+\n+            if short_name.startswith('initialize'):\n+                self.initializers.append(data)\n+            elif short_name == 'cleanup':\n+                self.cleanup = data\n+            else:\n+                self.callbacks.append(data)\n+\n+        return self.callbacks != []\n+\n+    def refresh(self, path):\n+        self.modified = False\n+\n+        try:\n+            st = os.stat(path)\n+\n+            # Not modified\n+            if st.st_mtime == self.mtime:\n+                return True\n+\n+            self.modified = True\n+            self.mtime = st.st_mtime\n+\n+            with codecs.open(path, encoding='utf-8') as fp:\n+                raw_content = fp.read()\n+\n+        except IOError:\n+            return False\n+\n+        return self.parse(raw_content)\n+\n+class TestSuite(object):\n+\n+    def __init__(self, path, output):\n+        self.path = path\n+        self.output = output\n+\n+    def should_generate(self, path):\n+        if not os.path.isfile(path):\n+            return True\n+\n+        if any(module.modified for module in self.modules.values()):\n+            return True\n+\n+        return False\n+\n+    def find_modules(self):\n+        modules = []\n+        for root, _, files in os.walk(self.path):\n+            module_root = root[len(self.path):]\n+            module_root = [c for c in module_root.split(os.sep) if c]\n+\n+            tests_in_module = fnmatch.filter(files, \"*.c\")\n+\n+            for test_file in tests_in_module:\n+                full_path = os.path.join(root, test_file)\n+                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n+\n+                modules.append((full_path, module_name))\n+\n+        return modules\n+\n+    def load_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        cache = {}\n+\n+        try:\n+            fp = open(path, 'rb')\n+            cache = pickle.load(fp)\n+            fp.close()\n+        except (IOError, ValueError):\n+            pass\n+\n+        return cache\n+\n+    def save_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        with open(path, 'wb') as cache:\n+            pickle.dump(self.modules, cache)\n+\n+    def load(self, force = False):\n+        module_data = self.find_modules()\n+        self.modules = {} if force else self.load_cache()\n+\n+        for path, name in module_data:\n+            if name not in self.modules:\n+                self.modules[name] = Module(name)\n+\n+            if not self.modules[name].refresh(path):\n+                del self.modules[name]\n+\n+    def disable(self, excluded):\n+        for exclude in excluded:\n+            for module in self.modules.values():\n+                name = module.clean_name()\n+                if name.startswith(exclude):\n+                    module.enabled = False\n+                    module.modified = True\n+\n+    def suite_count(self):\n+        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n+\n+    def callback_count(self):\n+        return sum(len(module.callbacks) for module in self.modules.values())\n+\n+    def write(self):\n+        output = os.path.join(self.output, 'clar.suite')\n+\n+        if not self.should_generate(output):\n+            return False\n+\n+        with open(output, 'w') as data:\n+            modules = sorted(self.modules.values(), key=lambda module: module.name)\n+\n+            for module in modules:\n+                t = Module.DeclarationTemplate(module)\n+                data.write(t.render())\n+\n+            for module in modules:\n+                t = Module.CallbacksTemplate(module)\n+                data.write(t.render())\n+\n+            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n+                Module.InfoTemplate(module).render() for module in modules\n+            ) + \"\\n};\\n\"\n+\n+            data.write(suites)\n+\n+            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n+            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n+\n+        self.save_cache()\n+        return True\n+\n+if __name__ == '__main__':\n+    from optparse import OptionParser\n+\n+    parser = OptionParser()\n+    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n+    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n+    parser.add_option('-o', '--output', dest='output')\n+\n+    options, args = parser.parse_args()\n+    if len(args) > 1:\n+        print(\"More than one path given\")\n+        sys.exit(1)\n+\n+    path = args.pop() if args else '.'\n+    output = options.output or path\n+    suite = TestSuite(path, output)\n+    suite.load(options.force)\n+    suite.disable(options.excluded)\n+    if suite.write():\n+        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n+\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nnew file mode 100644\nindex 0000000000..3e8ae0a3a5\n--- /dev/null\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -0,0 +1,5 @@\n+clar.suite\n+.clarcache\n+clar_test\n+*.o\n+\ndiff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\nnew file mode 100644\nindex 0000000000..93c6b2ad32\n--- /dev/null\n+++ b/t/unit-tests/clar/test/Makefile\n@@ -0,0 +1,39 @@\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+#\n+# Set up the path to the clar sources and to the fixtures directory\n+#\n+# The fixture path needs to be an absolute path so it can be used\n+# even after we have chdir'ed into the test directory while testing.\n+#\n+CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n+TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n+CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n+CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n+\n+CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n+\n+.PHONY: clean\n+\n+# list the objects that go into our test\n+objects = main.o sample.o\n+\n+# build the test executable itself\n+clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n+\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n+\n+# test object files depend on clar macros\n+$(objects) : $(CLAR_PATH)clar.h\n+\n+# build the clar.suite file of test metadata\n+clar.suite:\n+\tpython \"$(CLAR_PATH)generate.py\" .\n+\n+# remove all generated files\n+clean:\n+\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\ndiff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\nnew file mode 100644\nindex 0000000000..0fcaa639aa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/clar_test.h\n@@ -0,0 +1,16 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST__\n+#define __CLAR_TEST__\n+\n+/* Import the standard clar helper functions */\n+#include \"clar.h\"\n+\n+/* Your custom shared includes / defines here */\n+extern int global_test_counter;\n+\n+#endif\ndiff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\nnew file mode 100644\nindex 0000000000..59e56ad255\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c\n@@ -0,0 +1,40 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Sample main() for clar tests.\n+ *\n+ * You should write your own main routine for clar tests that does specific\n+ * setup and teardown as necessary for your application.  The only required\n+ * line is the call to `clar_test(argc, argv)`, which will execute the test\n+ * suite.  If you want to check the return value of the test application,\n+ * your main() should return the same value returned by clar_test().\n+ */\n+\n+int global_test_counter = 0;\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\tint ret;\n+\n+\t/* Your custom initialization here */\n+\tglobal_test_counter = 0;\n+\n+\t/* Run the test suite */\n+\tret = clar_test(argc, argv);\n+\n+\t/* Your custom cleanup here */\n+\tcl_assert_equal_i(8, global_test_counter);\n+\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\nnew file mode 100644\nindex 0000000000..a4d91b72fa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c.sample\n@@ -0,0 +1,27 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Minimal main() for clar tests.\n+ *\n+ * Modify this with any application specific setup or teardown that you need.\n+ * The only required line is the call to `clar_test(argc, argv)`, which will\n+ * execute the test suite.  If you want to check the return value of the test\n+ * application, main() should return the same value returned by clar_test().\n+ */\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\t/* Run the test suite */\n+\treturn clar_test(argc, argv);\n+}\ndiff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\nnew file mode 100644\nindex 0000000000..220f4aa98a\n--- /dev/null\n+++ b/t/unit-tests/clar/test/resources/test/file\n@@ -0,0 +1 @@\n+File\ndiff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\nnew file mode 100644\nindex 0000000000..faa1209262\n--- /dev/null\n+++ b/t/unit-tests/clar/test/sample.c\n@@ -0,0 +1,84 @@\n+#include \"clar_test.h\"\n+#include <sys/stat.h>\n+\n+static int file_size(const char *filename)\n+{\n+\tstruct stat st;\n+\n+\tif (stat(filename, &st) == 0)\n+\t\treturn (int)st.st_size;\n+\treturn -1;\n+}\n+\n+void test_sample__initialize(void)\n+{\n+\tglobal_test_counter++;\n+}\n+\n+void test_sample__cleanup(void)\n+{\n+\tcl_fixture_cleanup(\"test\");\n+\n+\tcl_assert(file_size(\"test/file\") == -1);\n+}\n+\n+void test_sample__1(void)\n+{\n+\tcl_assert(1);\n+\tcl_must_pass(0);  /* 0 == success */\n+\tcl_must_fail(-1); /* <0 == failure */\n+\tcl_must_pass(-1); /* demonstrate a failing call */\n+}\n+\n+void test_sample__2(void)\n+{\n+\tcl_fixture_sandbox(\"test\");\n+\n+\tcl_assert(file_size(\"test/nonexistent\") == -1);\n+\tcl_assert(file_size(\"test/file\") > 0);\n+\tcl_assert(100 == 101);\n+}\n+\n+void test_sample__strings(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_s(\"expected\", actual);\n+\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n+\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n+}\n+\n+void test_sample__strings_with_length(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_strn(\"expected_\", actual, 8);\n+\tcl_assert_equal_strn(\"exactly\", actual, 2);\n+\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n+\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n+}\n+\n+void test_sample__int(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i(100, value);\n+\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n+}\n+\n+void test_sample__int_fmt(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n+}\n+\n+void test_sample__bool(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n+\tcl_assert_equal_b(0, value);\n+}\n+\n+void test_sample__ptr(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n+\tcl_assert_equal_p(&actual, actual);\n+}\n-- \n2.46.0.dirty\n\n"},{"id":"500197","messageId":"a52ee59bf4115acf29a70ebb62e94e880698df26.1722952908.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722952908.git.ps@pks.im","subject":"[RFC PATCH v2 3/7] t/clar: fix whitespace errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-06T14:14:44Z","receivedAt":"2024-08-06T14:14:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Fix whitespace errors in the clar that make git-apply(1) unhappy. This\nhas been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/97\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n t/unit-tests/clar/generate.py     | 1 -\n t/unit-tests/clar/test/.gitignore | 1 -\n 3 files changed, 2 insertions(+), 5 deletions(-)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 0ba1479620..7c177f3525 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -25,9 +25,9 @@ find_tmp_path(char *buffer, size_t length)\n \tstatic const size_t var_count = 5;\n \tstatic const char *env_vars[] = {\n \t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n- \t};\n+\t};\n \n- \tsize_t i;\n+\tsize_t i;\n \n \tfor (i = 0; i < var_count; ++i) {\n \t\tconst char *env = getenv(env_vars[i]);\n@@ -151,4 +151,3 @@ const char *clar_sandbox_path(void)\n {\n \treturn _clar_path;\n }\n-\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nindex 931b4d60d9..80996ac3e7 100755\n--- a/t/unit-tests/clar/generate.py\n+++ b/t/unit-tests/clar/generate.py\n@@ -264,4 +264,3 @@ def write(self):\n     suite.disable(options.excluded)\n     if suite.write():\n         print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n-\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nindex 3e8ae0a3a5..a477d0c40c 100644\n--- a/t/unit-tests/clar/test/.gitignore\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -2,4 +2,3 @@ clar.suite\n .clarcache\n clar_test\n *.o\n-\n-- \n2.46.0.dirty\n\n"},{"id":"500198","messageId":"02fb86dfbcb4abb61d12c5f286bd2c3f5e0cdc66.1722952908.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722952908.git.ps@pks.im","subject":"[RFC PATCH v2 4/7] t/clar: fix compatibility with NonStop","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-06T14:14:49Z","receivedAt":"2024-08-06T14:14:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The NonStop platform does not have `mkdtemp()` available, which we rely\non in `build_sandbox_path()`. Fix this issue by using `mktemp()` and\n`mkdir()` instead on this platform.\n\nThis has been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/96\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 7c177f3525..e25057b7c4 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -120,6 +120,12 @@ static int build_sandbox_path(void)\n \tif (_mktemp(_clar_path) == NULL)\n \t\treturn -1;\n \n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(__TANDEM)\n+\tif (mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n \tif (mkdir(_clar_path, 0700) != 0)\n \t\treturn -1;\n #elif defined(_WIN32)\n-- \n2.46.0.dirty\n\n"},{"id":"500199","messageId":"848dc673c494d7771e1c1c64e8cd56788f9c6b8e.1722952908.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722952908.git.ps@pks.im","subject":"[RFC PATCH v2 5/7] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-06T14:14:53Z","receivedAt":"2024-08-06T14:14:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Wire up the clar unit testing framework by introducing a new\n\"unit-tests\" executable. In contrast to the existing framework, this\nwill result in a single executable for all test suites. The ability to\npick specific tests to execute is retained via functionality built into\nthe clar itself.\n\nNote that we need to be a bit careful about how we need to invalidate\nour Makefile rules. While we obviously have to regenerate the clar suite\nwhen our test suites change, we also have to invalidate it in case any\nof the test suites gets removed. We do so by using our typical pattern\nof creating a `GIT-TEST-SUITES` file that gets updated whenever the set\nof test suites changes, so that we can easily depend on that file.\n\nAnother specialty is that we generate a \"clar-decls.h\" file. The test\nfunctions are neither static, nor do they have external declarations.\nThis is because they are getting parsed via \"generate.py\", which then\ncreates the external generations that get populated into an array. These\ndeclarations are only seen by the main function though.\n\nThe consequence is that we will get a bunch of \"missing prototypes\"\nerrors from our compiler for each of these test functions. To fix those\nerrors, we extract the `extern` declarations from \"clar.suite\" and put\nthem into a standalone header that then gets included by each of our\nunit tests. This gets rid of compiler warnings for every function which\nhas been extracted by \"generate.py\". More importantly though, it does\n_not_ get rid of warnings in case a function really isn't being used by\nanything. Thus, it would cause a compiler error if a function name was\nmistyped and thus not picked up by \"generate.py\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Makefile                       | 36 ++++++++++++++++++++----\n t/Makefile                     |  1 +\n t/unit-tests/.gitignore        |  2 ++\n t/unit-tests/clar-generate.awk | 50 ++++++++++++++++++++++++++++++++++\n t/unit-tests/unit-test.c       | 18 ++++++++++++\n t/unit-tests/unit-test.h       |  3 ++\n 7 files changed, 106 insertions(+), 5 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\ndiff --git a/.gitignore b/.gitignore\nindex 8caf3700c2..6687bd6db4 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,6 +9,7 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n+/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex 674b0ac4e1..f56b14ad6d 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,6 +1334,11 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n+UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n+\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n@@ -2713,6 +2718,7 @@ OBJECTS += $(XDIFF_OBJS)\n OBJECTS += $(FUZZ_OBJS)\n OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n OBJECTS += $(UNIT_TEST_OBJS)\n+OBJECTS += $(UNIT_TESTS_OBJS)\n \n ifndef NO_CURL\n \tOBJECTS += http.o http-walker.o remote-curl.o\n@@ -3215,7 +3221,7 @@ endif\n \n test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))\n \n-all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)\n+all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n \n bin-wrappers/%: wrap-for-bin.sh\n \t$(call mkdir_p_parent_template)\n@@ -3262,7 +3268,7 @@ $(SP_OBJ): %.sp: %.c %.o\n .PHONY: sparse\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/unit-test.h $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n ifndef OPENSSL_SHA1\n \tEXCEPT_HDRS += sha1/openssl.h\n endif\n@@ -3646,7 +3652,7 @@ endif\n \n artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n \t\tGIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \\\n-\t\t$(UNIT_TEST_PROGS) $(MOFILES)\n+\t\t$(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) $(MOFILES)\n \t$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \\\n \t\tSHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'\n \ttest -n \"$(ARTIFACTS_DIRECTORY)\"\n@@ -3702,6 +3708,7 @@ cocciclean:\n \n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n+\t$(RM) GIT-TEST-SUITES $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/clar-decls.h\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3861,7 +3868,26 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n \n+GIT-TEST-SUITES: FORCE\n+\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n+\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n+\t\techo >&2 \"    * new test suites\"; \\\n+\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n+            fi\n+\n+$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n+\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n+\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n+\tdone >$@\n+$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n+\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n+$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR) -I$(UNIT_TEST_DIR)/clar\n+$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n+\t$(call mkdir_p_parent_template)\n+\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n+\n .PHONY: build-unit-tests unit-tests\n-build-unit-tests: $(UNIT_TEST_PROGS)\n-unit-tests: $(UNIT_TEST_PROGS) t/helper/test-tool$X\n+build-unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n+unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) t/helper/test-tool$X\n \t$(MAKE) -C t/ unit-tests\ndiff --git a/t/Makefile b/t/Makefile\nindex d2212de0b7..131ffd778f 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -48,6 +48,7 @@ CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.tes\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n+UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n UNIT_TESTS = $(sort $(UNIT_TEST_PROGRAMS))\n UNIT_TESTS_NO_DIR = $(notdir $(UNIT_TESTS))\n \ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex 5e56e040ec..d0632ec7f9 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1 +1,3 @@\n /bin\n+/clar.suite\n+/clar-decls.h\ndiff --git a/t/unit-tests/clar-generate.awk b/t/unit-tests/clar-generate.awk\nnew file mode 100644\nindex 0000000000..ab71ce6c9f\n--- /dev/null\n+++ b/t/unit-tests/clar-generate.awk\n@@ -0,0 +1,50 @@\n+function add_suite(suite, initialize, cleanup, count) {\n+       if (!suite) return\n+       suite_count++\n+       callback_count += count\n+       suites = suites \"    {\\n\"\n+       suites = suites \"        \\\"\" suite \"\\\",\\n\"\n+       suites = suites \"        \" initialize \",\\n\"\n+       suites = suites \"        \" cleanup \",\\n\"\n+       suites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n+       suites = suites \"    },\\n\"\n+}\n+\n+BEGIN {\n+       suites = \"static struct clar_suite _clar_suites[] = {\\n\"\n+}\n+\n+{\n+       print\n+       name = $3; sub(/\\(.*$/, \"\", name)\n+       suite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n+       short_name = name; sub(/^.*__/, \"\", short_name)\n+       cb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n+       if (suite != prev_suite) {\n+               add_suite(prev_suite, initialize, cleanup, count)\n+               if (callbacks) callbacks = callbacks \"};\\n\"\n+               callbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n+               initialize = \"{ NULL, NULL }\"\n+               cleanup = \"{ NULL, NULL }\"\n+               count = 0\n+               prev_suite = suite\n+       }\n+       if (short_name == \"initialize\") {\n+               initialize = cb\n+       } else if (short_name == \"cleanup\") {\n+               cleanup = cb\n+       } else {\n+               callbacks = callbacks \"    \" cb \",\\n\"\n+               count++\n+       }\n+}\n+\n+END {\n+       add_suite(suite, initialize, cleanup, count)\n+       suites = suites \"};\"\n+       if (callbacks) callbacks = callbacks \"};\"\n+       print callbacks\n+       print suites\n+       print \"static const size_t _clar_suite_count = \" suite_count \";\"\n+       print \"static const size_t _clar_callback_count = \" callback_count \";\"\n+}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nnew file mode 100644\nindex 0000000000..32a81299e9\n--- /dev/null\n+++ b/t/unit-tests/unit-test.c\n@@ -0,0 +1,18 @@\n+#include \"unit-test.h\"\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tconst char **argv_copy;\n+\tint ret;\n+\n+\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n+\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tCOPY_ARRAY(argv_copy, argv, argc);\n+\targv_copy[argc++] = \"-t\";\n+\targv_copy[argc] = NULL;\n+\n+\tret = clar_test(argc, (char **) argv_copy);\n+\n+\tfree(argv_copy);\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nnew file mode 100644\nindex 0000000000..99d59df1b0\n--- /dev/null\n+++ b/t/unit-tests/unit-test.h\n@@ -0,0 +1,3 @@\n+#include \"git-compat-util.h\"\n+#include \"clar.h\"\n+#include \"clar-decls.h\"\n-- \n2.46.0.dirty\n\n"},{"id":"500200","messageId":"578e657269dd448d3de2964bba03fbff869560ba.1722952908.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722952908.git.ps@pks.im","subject":"[RFC PATCH v2 6/7] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-06T14:14:58Z","receivedAt":"2024-08-06T14:15:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the strvec tests to use the new clar unit testing framework.\nThis is a first test balloon that demonstrates how the testing infra for\nclar-based tests looks like.\n\nThe tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\nrunning that binary, it generates TAP output:\n\n    # ./t/unit-tests/bin/unit-tests\n    TAP version 13\n    # start of suite 1: strvec\n    ok 1 - strvec::init\n    ok 2 - strvec::dynamic_init\n    ok 3 - strvec::clear\n    ok 4 - strvec::push\n    ok 5 - strvec::pushft_pushf\n    ok 6 - strvec::pushl\n    ok 7 - strvec::pushv\n    ok 8 - strvec::replace_at_head\n    ok 9 - strvec::replace_at_tail\n    ok 10 - strvec::replace_in_between\n    ok 11 - strvec::replace_with_substring\n    ok 12 - strvec::remove_at_head\n    ok 13 - strvec::remove_at_tail\n    ok 14 - strvec::remove_in_between\n    ok 15 - strvec::pop_empty_array\n    ok 16 - strvec::pop_non_empty_array\n    ok 17 - strvec::split_empty_string\n    ok 18 - strvec::split_single_item\n    ok 19 - strvec::split_multiple_items\n    ok 20 - strvec::split_whitespace_only\n    ok 21 - strvec::split_multiple_consecutive_whitespaces\n    ok 22 - strvec::detach\n    1..22\n\nThe binary also supports some parameters that allow us to run only a\nsubset of unit tests or alter the output:\n\n    $ ./t/unit-tests/bin/unit-tests -h\n    Usage: ./t/unit-tests/bin/unit-tests [options]\n\n    Options:\n      -sname        Run only the suite with `name` (can go to individual test name)\n      -iname        Include the suite with `name`\n      -xname        Exclude the suite with `name`\n      -v            Increase verbosity (show suite names)\n      -q            Only report tests that had an error\n      -Q            Quit as soon as a test fails\n      -t            Display results in tap format\n      -l            Print suite names\n      -r[filename]  Write summary file (to the optional filename)\n\nFurthermore, running `make unit-tests` runs the binary along with all\nthe other unit tests we have.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                              |   2 +-\n t/unit-tests/{t-strvec.c => strvec.c} | 119 ++++++++++----------------\n t/unit-tests/unit-test.c              |   3 +-\n 3 files changed, 45 insertions(+), 79 deletions(-)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n\ndiff --git a/Makefile b/Makefile\nindex f56b14ad6d..cf76f9b353 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,6 +1334,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n@@ -1351,7 +1352,6 @@ UNIT_TEST_PROGRAMS += t-reftable-merged\n UNIT_TEST_PROGRAMS += t-reftable-record\n UNIT_TEST_PROGRAMS += t-strbuf\n UNIT_TEST_PROGRAMS += t-strcmp-offset\n-UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-trailer\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/strvec.c\nsimilarity index 54%\nrename from t/unit-tests/t-strvec.c\nrename to t/unit-tests/strvec.c\nindex fa1a041469..d782c5f73b 100644\n--- a/t/unit-tests/t-strvec.c\n+++ b/t/unit-tests/strvec.c\n@@ -1,52 +1,46 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n \n #define check_strvec(vec, ...) \\\n \tdo { \\\n \t\tconst char *expect[] = { __VA_ARGS__ }; \\\n-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n-\t\t\t\t\t\t (uintmax_t)i); \\\n-\t\t\t\t\tbreak; \\\n-\t\t\t\t} \\\n-\t\t\t} \\\n-\t\t} \\\n+\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n+\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n+\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n+\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n+\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n \t} while (0)\n \n-static void t_static_init(void)\n+void test_strvec__init(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert(vec.nr == 0);\n+\tcl_assert(vec.alloc == 0);\n }\n \n-static void t_dynamic_init(void)\n+void test_strvec__dynamic_init(void)\n {\n \tstruct strvec vec;\n \tstrvec_init(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert(vec.nr == 0);\n+\tcl_assert(vec.alloc == 0);\n }\n \n-static void t_clear(void)\n+void test_strvec__clear(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_push(&vec, \"foo\");\n \tstrvec_clear(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert(vec.nr == 0);\n+\tcl_assert(vec.alloc == 0);\n }\n \n-static void t_push(void)\n+void test_strvec__push(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \n@@ -59,7 +53,7 @@ static void t_push(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushf(void)\n+void test_strvec__pushf(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushf(&vec, \"foo: %d\", 1);\n@@ -67,7 +61,7 @@ static void t_pushf(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushl(void)\n+void test_strvec__pushl(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -75,7 +69,7 @@ static void t_pushl(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushv(void)\n+void test_strvec__pushv(void)\n {\n \tconst char *strings[] = {\n \t\t\"foo\", \"bar\", \"baz\", NULL,\n@@ -88,7 +82,7 @@ static void t_pushv(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_head(void)\n+void test_strvec__replace_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -97,7 +91,7 @@ static void t_replace_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_tail(void)\n+void test_strvec__replace_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -106,7 +100,7 @@ static void t_replace_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_in_between(void)\n+void test_strvec__replace_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -115,7 +109,7 @@ static void t_replace_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_with_substring(void)\n+void test_strvec__replace_with_substring(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", NULL);\n@@ -124,7 +118,7 @@ static void t_replace_with_substring(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_head(void)\n+void test_strvec__remove_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -133,7 +127,7 @@ static void t_remove_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_tail(void)\n+void test_strvec__remove_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -142,7 +136,7 @@ static void t_remove_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_in_between(void)\n+void test_strvec__remove_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -151,7 +145,7 @@ static void t_remove_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_empty_array(void)\n+void test_strvec__pop_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pop(&vec);\n@@ -159,7 +153,7 @@ static void t_pop_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_non_empty_array(void)\n+void test_strvec__pop_non_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -168,7 +162,7 @@ static void t_pop_non_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_empty_string(void)\n+void test_strvec__split_empty_string(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"\");\n@@ -176,7 +170,7 @@ static void t_split_empty_string(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_single_item(void)\n+void test_strvec__split_single_item(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\");\n@@ -184,7 +178,7 @@ static void t_split_single_item(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_items(void)\n+void test_strvec__split_multiple_items(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo bar baz\");\n@@ -192,7 +186,7 @@ static void t_split_multiple_items(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_whitespace_only(void)\n+void test_strvec__split_whitespace_only(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \" \\t\\n\");\n@@ -200,7 +194,7 @@ static void t_split_whitespace_only(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_consecutive_whitespaces(void)\n+void test_strvec__split_multiple_consecutive_whitespaces(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\\n\\t bar\");\n@@ -208,7 +202,7 @@ static void t_split_multiple_consecutive_whitespaces(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_detach(void)\n+void test_strvec__detach(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tconst char **detached;\n@@ -216,40 +210,13 @@ static void t_detach(void)\n \tstrvec_push(&vec, \"foo\");\n \n \tdetached = strvec_detach(&vec);\n-\tcheck_str(detached[0], \"foo\");\n-\tcheck_pointer_eq(detached[1], NULL);\n+\tcl_assert_equal_s(detached[0], \"foo\");\n+\tcl_assert_equal_p(detached[1], NULL);\n \n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert(vec.nr == 0);\n+\tcl_assert(vec.alloc == 0);\n \n \tfree((char *) detached[0]);\n \tfree(detached);\n }\n-\n-int cmd_main(int argc, const char **argv)\n-{\n-\tTEST(t_static_init(), \"static initialization\");\n-\tTEST(t_dynamic_init(), \"dynamic initialization\");\n-\tTEST(t_clear(), \"clear\");\n-\tTEST(t_push(), \"push\");\n-\tTEST(t_pushf(), \"pushf\");\n-\tTEST(t_pushl(), \"pushl\");\n-\tTEST(t_pushv(), \"pushv\");\n-\tTEST(t_replace_at_head(), \"replace at head\");\n-\tTEST(t_replace_in_between(), \"replace in between\");\n-\tTEST(t_replace_at_tail(), \"replace at tail\");\n-\tTEST(t_replace_with_substring(), \"replace with substring\");\n-\tTEST(t_remove_at_head(), \"remove at head\");\n-\tTEST(t_remove_in_between(), \"remove in between\");\n-\tTEST(t_remove_at_tail(), \"remove at tail\");\n-\tTEST(t_pop_empty_array(), \"pop with empty array\");\n-\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n-\tTEST(t_split_empty_string(), \"split empty string\");\n-\tTEST(t_split_single_item(), \"split single item\");\n-\tTEST(t_split_multiple_items(), \"split multiple items\");\n-\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n-\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n-\tTEST(t_detach(), \"detach\");\n-\treturn test_done();\n-}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nindex 32a81299e9..82b7635e6a 100644\n--- a/t/unit-tests/unit-test.c\n+++ b/t/unit-tests/unit-test.c\n@@ -6,10 +6,9 @@ int cmd_main(int argc, const char **argv)\n \tint ret;\n \n \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n-\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tALLOC_ARRAY(argv_copy, argc + 1);\n \tCOPY_ARRAY(argv_copy, argv, argc);\n \targv_copy[argc++] = \"-t\";\n-\targv_copy[argc] = NULL;\n \n \tret = clar_test(argc, (char **) argv_copy);\n \n-- \n2.46.0.dirty\n\n"},{"id":"500201","messageId":"238de33b9381dceaa8c4175251b3d25c2ef78777.1722952908.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722952908.git.ps@pks.im","subject":"[RFC PATCH v2 7/7] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-06T14:15:03Z","receivedAt":"2024-08-06T14:15:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the ctype tests to use the new clar unit testing framework.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                            |  2 +-\n t/unit-tests/{t-ctype.c => ctype.c} | 71 +++++++++++++++++++++++------\n 2 files changed, 59 insertions(+), 14 deletions(-)\n rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n\ndiff --git a/Makefile b/Makefile\nindex cf76f9b353..aa2bd5cec1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,13 +1334,13 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += ctype\n UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n \n-UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n UNIT_TEST_PROGRAMS += t-mem-pool\ndiff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/ctype.c\nsimilarity index 71%\nrename from t/unit-tests/t-ctype.c\nrename to t/unit-tests/ctype.c\nindex d6ac1fe678..311df3a539 100644\n--- a/t/unit-tests/t-ctype.c\n+++ b/t/unit-tests/ctype.c\n@@ -1,18 +1,12 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n \n #define TEST_CHAR_CLASS(class, string) do { \\\n \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n-\tint skip = test__run_begin(); \\\n-\tif (!skip) { \\\n-\t\tfor (int i = 0; i < 256; i++) { \\\n-\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n-\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n-\t\t} \\\n-\t\tcheck(!class(EOF)); \\\n-\t} \\\n-\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n+\tfor (int i = 0; i < 256; i++) \\\n+\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n+\tcl_assert(!class(EOF)); \\\n } while (0)\n \n #define DIGIT \"0123456789\"\n@@ -33,21 +27,72 @@\n \t\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\" \\\n \t\"\\x7f\"\n \n-int cmd_main(int argc, const char **argv) {\n+void test_ctype__isspace(void)\n+{\n \tTEST_CHAR_CLASS(isspace, \" \\n\\r\\t\");\n+}\n+\n+void test_ctype__isdigit(void)\n+{\n \tTEST_CHAR_CLASS(isdigit, DIGIT);\n+}\n+\n+void test_ctype__isalpha(void)\n+{\n \tTEST_CHAR_CLASS(isalpha, LOWER UPPER);\n+}\n+\n+void test_ctype__isalnum(void)\n+{\n \tTEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);\n+}\n+\n+void test_ctype__is_glob_special(void)\n+{\n \tTEST_CHAR_CLASS(is_glob_special, \"*?[\\\\\");\n+}\n+\n+void test_ctype__is_regex_special(void)\n+{\n \tTEST_CHAR_CLASS(is_regex_special, \"$()*+.?[\\\\^{|\");\n+}\n+\n+void test_ctype__is_pathspec_magic(void)\n+{\n \tTEST_CHAR_CLASS(is_pathspec_magic, \"!\\\"#%&',-/:;<=>@_`~\");\n+}\n+\n+void test_ctype__isascii(void)\n+{\n \tTEST_CHAR_CLASS(isascii, ASCII);\n+}\n+\n+void test_ctype__islower(void)\n+{\n \tTEST_CHAR_CLASS(islower, LOWER);\n+}\n+\n+void test_ctype__isupper(void)\n+{\n \tTEST_CHAR_CLASS(isupper, UPPER);\n+}\n+\n+void test_ctype__iscntrl(void)\n+{\n \tTEST_CHAR_CLASS(iscntrl, CNTRL);\n+}\n+\n+void test_ctype__ispunct(void)\n+{\n \tTEST_CHAR_CLASS(ispunct, PUNCT);\n+}\n+\n+void test_ctype__isxdigit(void)\n+{\n \tTEST_CHAR_CLASS(isxdigit, DIGIT \"abcdefABCDEF\");\n-\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n+}\n \n-\treturn test_done();\n+void test_ctype__isprint(void)\n+{\n+\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n }\n-- \n2.46.0.dirty\n\n"},{"id":"500254","messageId":"36e3ukvajteiywmsca7vohyk35ehqsgs5rj7njy4ssmykfb4om@sbolkz7xfafe","threadId":"61874","inReplyTo":"6a88cf22a5de3faa77cb22998d27f0c1b63bd4ff.1722952908.git.ps@pks.im","subject":"Re: [RFC PATCH v2 2/7] t: import the clar unit testing framework","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2024-08-06T22:18:25Z","receivedAt":"2024-08-06T22:18:32Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2024.08.06 16:14, Patrick Steinhardt wrote:\n> Import the clar unit testing framework at commit faa8419 (Merge pull\n> request #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\n> will be wired up in subsequent commits.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/technical/unit-tests.txt     |   2 +\n>  Makefile                                   |   4 +-\n>  t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n>  t/unit-tests/clar/COPYING                  |  15 +\n>  t/unit-tests/clar/README.md                | 329 ++++++++\n>  t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n>  t/unit-tests/clar/clar.h                   | 173 +++++\n>  t/unit-tests/clar/clar/fixtures.h          |  50 ++\n>  t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n>  t/unit-tests/clar/clar/print.h             | 211 ++++++\n>  t/unit-tests/clar/clar/sandbox.h           | 154 ++++\n>  t/unit-tests/clar/clar/summary.h           | 143 ++++\n>  t/unit-tests/clar/generate.py              | 267 +++++++\n>  t/unit-tests/clar/test/.gitignore          |   5 +\n>  t/unit-tests/clar/test/Makefile            |  39 +\n>  t/unit-tests/clar/test/clar_test.h         |  16 +\n>  t/unit-tests/clar/test/main.c              |  40 +\n>  t/unit-tests/clar/test/main.c.sample       |  27 +\n>  t/unit-tests/clar/test/resources/test/file |   1 +\n>  t/unit-tests/clar/test/sample.c            |  84 ++\n>  20 files changed, 2946 insertions(+), 1 deletion(-)\n>  create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n>  create mode 100644 t/unit-tests/clar/COPYING\n>  create mode 100644 t/unit-tests/clar/README.md\n>  create mode 100644 t/unit-tests/clar/clar.c\n>  create mode 100644 t/unit-tests/clar/clar.h\n>  create mode 100644 t/unit-tests/clar/clar/fixtures.h\n>  create mode 100644 t/unit-tests/clar/clar/fs.h\n>  create mode 100644 t/unit-tests/clar/clar/print.h\n>  create mode 100644 t/unit-tests/clar/clar/sandbox.h\n>  create mode 100644 t/unit-tests/clar/clar/summary.h\n>  create mode 100755 t/unit-tests/clar/generate.py\n>  create mode 100644 t/unit-tests/clar/test/.gitignore\n>  create mode 100644 t/unit-tests/clar/test/Makefile\n>  create mode 100644 t/unit-tests/clar/test/clar_test.h\n>  create mode 100644 t/unit-tests/clar/test/main.c\n>  create mode 100644 t/unit-tests/clar/test/main.c.sample\n>  create mode 100644 t/unit-tests/clar/test/resources/test/file\n>  create mode 100644 t/unit-tests/clar/test/sample.c\n> \n> diff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt\n> index 206037ffb1..fc201d4b3d 100644\n> --- a/Documentation/technical/unit-tests.txt\n> +++ b/Documentation/technical/unit-tests.txt\n> @@ -203,6 +203,7 @@ GitHub / GitLab stars to estimate this.\n>  :criterion: https://github.com/Snaipe/Criterion[Criterion]\n>  :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n>  :check: https://libcheck.github.io/check/[Check]\n> +:clar: https://github.com/clar-test/clar[Clar]\n>  \n>  [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n>  |=====\n> @@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n>  {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n>  {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n>  {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n> +{clar},{lgpl},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n>  |=====\n\nIsn't clar ISC-licensed, not LGPL?\nhttps://github.com/clar-test/clar/blob/main/COPYING\n\n"},{"id":"500255","messageId":"6a6z3xduic3c6v27zeag5mxclupb4rc7a3gvoov5odpa7vqz4u@glpita3ojyxx","threadId":"61874","inReplyTo":"578e657269dd448d3de2964bba03fbff869560ba.1722952908.git.ps@pks.im","subject":"Re: [RFC PATCH v2 6/7] t/unit-tests: convert strvec tests to use clar","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2024-08-06T23:05:46Z","receivedAt":"2024-08-06T23:05:52Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2024.08.06 16:14, Patrick Steinhardt wrote:\n> Convert the strvec tests to use the new clar unit testing framework.\n> This is a first test balloon that demonstrates how the testing infra for\n> clar-based tests looks like.\n> \n> The tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\n> running that binary, it generates TAP output:\n> \n>     # ./t/unit-tests/bin/unit-tests\n>     TAP version 13\n>     # start of suite 1: strvec\n>     ok 1 - strvec::init\n>     ok 2 - strvec::dynamic_init\n>     ok 3 - strvec::clear\n>     ok 4 - strvec::push\n>     ok 5 - strvec::pushft_pushf\n>     ok 6 - strvec::pushl\n>     ok 7 - strvec::pushv\n>     ok 8 - strvec::replace_at_head\n>     ok 9 - strvec::replace_at_tail\n>     ok 10 - strvec::replace_in_between\n>     ok 11 - strvec::replace_with_substring\n>     ok 12 - strvec::remove_at_head\n>     ok 13 - strvec::remove_at_tail\n>     ok 14 - strvec::remove_in_between\n>     ok 15 - strvec::pop_empty_array\n>     ok 16 - strvec::pop_non_empty_array\n>     ok 17 - strvec::split_empty_string\n>     ok 18 - strvec::split_single_item\n>     ok 19 - strvec::split_multiple_items\n>     ok 20 - strvec::split_whitespace_only\n>     ok 21 - strvec::split_multiple_consecutive_whitespaces\n>     ok 22 - strvec::detach\n>     1..22\n> \n> The binary also supports some parameters that allow us to run only a\n> subset of unit tests or alter the output:\n> \n>     $ ./t/unit-tests/bin/unit-tests -h\n>     Usage: ./t/unit-tests/bin/unit-tests [options]\n> \n>     Options:\n>       -sname        Run only the suite with `name` (can go to individual test name)\n>       -iname        Include the suite with `name`\n>       -xname        Exclude the suite with `name`\n>       -v            Increase verbosity (show suite names)\n>       -q            Only report tests that had an error\n>       -Q            Quit as soon as a test fails\n>       -t            Display results in tap format\n>       -l            Print suite names\n>       -r[filename]  Write summary file (to the optional filename)\n> \n> Furthermore, running `make unit-tests` runs the binary along with all\n> the other unit tests we have.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Makefile                              |   2 +-\n>  t/unit-tests/{t-strvec.c => strvec.c} | 119 ++++++++++----------------\n>  t/unit-tests/unit-test.c              |   3 +-\n>  3 files changed, 45 insertions(+), 79 deletions(-)\n>  rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n> \n> diff --git a/Makefile b/Makefile\n> index f56b14ad6d..cf76f9b353 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -1334,6 +1334,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n>  THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n>  THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n>  \n> +UNIT_TESTS_SUITES += strvec\n>  UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n>  UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n>  UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n> @@ -1351,7 +1352,6 @@ UNIT_TEST_PROGRAMS += t-reftable-merged\n>  UNIT_TEST_PROGRAMS += t-reftable-record\n>  UNIT_TEST_PROGRAMS += t-strbuf\n>  UNIT_TEST_PROGRAMS += t-strcmp-offset\n> -UNIT_TEST_PROGRAMS += t-strvec\n>  UNIT_TEST_PROGRAMS += t-trailer\n>  UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n>  UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\n> diff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/strvec.c\n> similarity index 54%\n> rename from t/unit-tests/t-strvec.c\n> rename to t/unit-tests/strvec.c\n> index fa1a041469..d782c5f73b 100644\n> --- a/t/unit-tests/t-strvec.c\n> +++ b/t/unit-tests/strvec.c\n> @@ -1,52 +1,46 @@\n> -#include \"test-lib.h\"\n> +#include \"unit-test.h\"\n>  #include \"strbuf.h\"\n>  #include \"strvec.h\"\n>  \n>  #define check_strvec(vec, ...) \\\n>  \tdo { \\\n>  \t\tconst char *expect[] = { __VA_ARGS__ }; \\\n> -\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n> -\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n> -\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n> -\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n> -\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n> -\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n> -\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n> -\t\t\t\t\t\t (uintmax_t)i); \\\n> -\t\t\t\t\tbreak; \\\n> -\t\t\t\t} \\\n> -\t\t\t} \\\n> -\t\t} \\\n> +\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n> +\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n> +\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n> +\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n> +\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n> +\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n>  \t} while (0)\n>  \n> -static void t_static_init(void)\n> +void test_strvec__init(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n> -\tcheck_pointer_eq(vec.v, empty_strvec);\n> -\tcheck_uint(vec.nr, ==, 0);\n> -\tcheck_uint(vec.alloc, ==, 0);\n> +\tcl_assert_equal_p(vec.v, empty_strvec);\n> +\tcl_assert(vec.nr == 0);\n> +\tcl_assert(vec.alloc == 0);\n\nIs there a reason you used cl_assert() instead of cl_assert_equal_i()\nfor the .nr and .alloc checks here and below?\n\n\n>  }\n>  \n> -static void t_dynamic_init(void)\n> +void test_strvec__dynamic_init(void)\n>  {\n>  \tstruct strvec vec;\n>  \tstrvec_init(&vec);\n> -\tcheck_pointer_eq(vec.v, empty_strvec);\n> -\tcheck_uint(vec.nr, ==, 0);\n> -\tcheck_uint(vec.alloc, ==, 0);\n> +\tcl_assert_equal_p(vec.v, empty_strvec);\n> +\tcl_assert(vec.nr == 0);\n> +\tcl_assert(vec.alloc == 0);\n>  }\n>  \n> -static void t_clear(void)\n> +void test_strvec__clear(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_push(&vec, \"foo\");\n>  \tstrvec_clear(&vec);\n> -\tcheck_pointer_eq(vec.v, empty_strvec);\n> -\tcheck_uint(vec.nr, ==, 0);\n> -\tcheck_uint(vec.alloc, ==, 0);\n> +\tcl_assert_equal_p(vec.v, empty_strvec);\n> +\tcl_assert(vec.nr == 0);\n> +\tcl_assert(vec.alloc == 0);\n>  }\n>  \n> -static void t_push(void)\n> +void test_strvec__push(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \n> @@ -59,7 +53,7 @@ static void t_push(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_pushf(void)\n> +void test_strvec__pushf(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushf(&vec, \"foo: %d\", 1);\n> @@ -67,7 +61,7 @@ static void t_pushf(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_pushl(void)\n> +void test_strvec__pushl(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> @@ -75,7 +69,7 @@ static void t_pushl(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_pushv(void)\n> +void test_strvec__pushv(void)\n>  {\n>  \tconst char *strings[] = {\n>  \t\t\"foo\", \"bar\", \"baz\", NULL,\n> @@ -88,7 +82,7 @@ static void t_pushv(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_replace_at_head(void)\n> +void test_strvec__replace_at_head(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> @@ -97,7 +91,7 @@ static void t_replace_at_head(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_replace_at_tail(void)\n> +void test_strvec__replace_at_tail(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> @@ -106,7 +100,7 @@ static void t_replace_at_tail(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_replace_in_between(void)\n> +void test_strvec__replace_in_between(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> @@ -115,7 +109,7 @@ static void t_replace_in_between(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_replace_with_substring(void)\n> +void test_strvec__replace_with_substring(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", NULL);\n> @@ -124,7 +118,7 @@ static void t_replace_with_substring(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_remove_at_head(void)\n> +void test_strvec__remove_at_head(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> @@ -133,7 +127,7 @@ static void t_remove_at_head(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_remove_at_tail(void)\n> +void test_strvec__remove_at_tail(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> @@ -142,7 +136,7 @@ static void t_remove_at_tail(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_remove_in_between(void)\n> +void test_strvec__remove_in_between(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> @@ -151,7 +145,7 @@ static void t_remove_in_between(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_pop_empty_array(void)\n> +void test_strvec__pop_empty_array(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pop(&vec);\n> @@ -159,7 +153,7 @@ static void t_pop_empty_array(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_pop_non_empty_array(void)\n> +void test_strvec__pop_non_empty_array(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> @@ -168,7 +162,7 @@ static void t_pop_non_empty_array(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_split_empty_string(void)\n> +void test_strvec__split_empty_string(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_split(&vec, \"\");\n> @@ -176,7 +170,7 @@ static void t_split_empty_string(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_split_single_item(void)\n> +void test_strvec__split_single_item(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_split(&vec, \"foo\");\n> @@ -184,7 +178,7 @@ static void t_split_single_item(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_split_multiple_items(void)\n> +void test_strvec__split_multiple_items(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_split(&vec, \"foo bar baz\");\n> @@ -192,7 +186,7 @@ static void t_split_multiple_items(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_split_whitespace_only(void)\n> +void test_strvec__split_whitespace_only(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_split(&vec, \" \\t\\n\");\n> @@ -200,7 +194,7 @@ static void t_split_whitespace_only(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_split_multiple_consecutive_whitespaces(void)\n> +void test_strvec__split_multiple_consecutive_whitespaces(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tstrvec_split(&vec, \"foo\\n\\t bar\");\n> @@ -208,7 +202,7 @@ static void t_split_multiple_consecutive_whitespaces(void)\n>  \tstrvec_clear(&vec);\n>  }\n>  \n> -static void t_detach(void)\n> +void test_strvec__detach(void)\n>  {\n>  \tstruct strvec vec = STRVEC_INIT;\n>  \tconst char **detached;\n> @@ -216,40 +210,13 @@ static void t_detach(void)\n>  \tstrvec_push(&vec, \"foo\");\n>  \n>  \tdetached = strvec_detach(&vec);\n> -\tcheck_str(detached[0], \"foo\");\n> -\tcheck_pointer_eq(detached[1], NULL);\n> +\tcl_assert_equal_s(detached[0], \"foo\");\n> +\tcl_assert_equal_p(detached[1], NULL);\n>  \n> -\tcheck_pointer_eq(vec.v, empty_strvec);\n> -\tcheck_uint(vec.nr, ==, 0);\n> -\tcheck_uint(vec.alloc, ==, 0);\n> +\tcl_assert_equal_p(vec.v, empty_strvec);\n> +\tcl_assert(vec.nr == 0);\n> +\tcl_assert(vec.alloc == 0);\n>  \n>  \tfree((char *) detached[0]);\n>  \tfree(detached);\n>  }\n> -\n> -int cmd_main(int argc, const char **argv)\n> -{\n> -\tTEST(t_static_init(), \"static initialization\");\n> -\tTEST(t_dynamic_init(), \"dynamic initialization\");\n> -\tTEST(t_clear(), \"clear\");\n> -\tTEST(t_push(), \"push\");\n> -\tTEST(t_pushf(), \"pushf\");\n> -\tTEST(t_pushl(), \"pushl\");\n> -\tTEST(t_pushv(), \"pushv\");\n> -\tTEST(t_replace_at_head(), \"replace at head\");\n> -\tTEST(t_replace_in_between(), \"replace in between\");\n> -\tTEST(t_replace_at_tail(), \"replace at tail\");\n> -\tTEST(t_replace_with_substring(), \"replace with substring\");\n> -\tTEST(t_remove_at_head(), \"remove at head\");\n> -\tTEST(t_remove_in_between(), \"remove in between\");\n> -\tTEST(t_remove_at_tail(), \"remove at tail\");\n> -\tTEST(t_pop_empty_array(), \"pop with empty array\");\n> -\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n> -\tTEST(t_split_empty_string(), \"split empty string\");\n> -\tTEST(t_split_single_item(), \"split single item\");\n> -\tTEST(t_split_multiple_items(), \"split multiple items\");\n> -\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n> -\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n> -\tTEST(t_detach(), \"detach\");\n> -\treturn test_done();\n> -}\n> diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\n> index 32a81299e9..82b7635e6a 100644\n> --- a/t/unit-tests/unit-test.c\n> +++ b/t/unit-tests/unit-test.c\n> @@ -6,10 +6,9 @@ int cmd_main(int argc, const char **argv)\n>  \tint ret;\n>  \n>  \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n> -\tALLOC_ARRAY(argv_copy, argc + 2);\n> +\tALLOC_ARRAY(argv_copy, argc + 1);\n>  \tCOPY_ARRAY(argv_copy, argv, argc);\n>  \targv_copy[argc++] = \"-t\";\n> -\targv_copy[argc] = NULL;\n>  \n>  \tret = clar_test(argc, (char **) argv_copy);\n>  \n> -- \n> 2.46.0.dirty\n> \n\n\n"},{"id":"500266","messageId":"ZrMLhzVCq0Ox9nT7@tanuki","threadId":"61874","inReplyTo":"36e3ukvajteiywmsca7vohyk35ehqsgs5rj7njy4ssmykfb4om@sbolkz7xfafe","subject":"Re: [RFC PATCH v2 2/7] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-07T05:52:07Z","receivedAt":"2024-08-07T05:52:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 06, 2024 at 03:18:25PM -0700, Josh Steadmon wrote:\n> On 2024.08.06 16:14, Patrick Steinhardt wrote:\n> > @@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n> >  {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n> >  {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n> >  {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n> > +{clar},{lgpl},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n> >  |=====\n> \n> Isn't clar ISC-licensed, not LGPL?\n> https://github.com/clar-test/clar/blob/main/COPYING\n\nYes it is, and that's also what I said in other mails. Guess this here\nwas a copy-paste error, thanks for spotting it!\n\nPatrick\n"},{"id":"500267","messageId":"ZrMLjjaz79s7O8B0@tanuki","threadId":"61874","inReplyTo":"6a6z3xduic3c6v27zeag5mxclupb4rc7a3gvoov5odpa7vqz4u@glpita3ojyxx","subject":"Re: [RFC PATCH v2 6/7] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-07T05:52:14Z","receivedAt":"2024-08-07T05:52:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 06, 2024 at 04:05:46PM -0700, Josh Steadmon wrote:\n> On 2024.08.06 16:14, Patrick Steinhardt wrote:\n> > -static void t_static_init(void)\n> > +void test_strvec__init(void)\n> >  {\n> >  \tstruct strvec vec = STRVEC_INIT;\n> > -\tcheck_pointer_eq(vec.v, empty_strvec);\n> > -\tcheck_uint(vec.nr, ==, 0);\n> > -\tcheck_uint(vec.alloc, ==, 0);\n> > +\tcl_assert_equal_p(vec.v, empty_strvec);\n> > +\tcl_assert(vec.nr == 0);\n> > +\tcl_assert(vec.alloc == 0);\n> \n> Is there a reason you used cl_assert() instead of cl_assert_equal_i()\n> for the .nr and .alloc checks here and below?\n\nNo, there isn't. Let me adapt these indeed, thanks!\n\nPatrick\n"},{"id":"500393","messageId":"cover.1723095269.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[RFC PATCH v3 0/7] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-08T05:38:08Z","receivedAt":"2024-08-08T05:38:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the third version of my RFC patch series that introduces the\nclar testing framework into our unit tests. The intent is to not have to\nhand-craft all features of a proper unit testing framework, while still\nnot painting us into a corner. As such, the clar itself is small and\nextensible while still bringing some nice features to the table.\n\nChanges compared to v2:\n\n  - Fix a copy/paste error for the clar license. It's ISC, not LGPL.\n\n  - Include \"clar.h\" via \"clar/clar.h\" such that we do not have to add\n    \"clar/\" as in preprocessor include directive.\n\n  - Adapt strvec unit test to use `cl_assert_equal_i()` instead of\n    `cl_assert()`.\n\nThanks!\n\nPatrick\n\nPatrick Steinhardt (7):\n  t: do not pass GIT_TEST_OPTS to unit tests with prove\n  t: import the clar unit testing framework\n  t/clar: fix whitespace errors\n  t/clar: fix compatibility with NonStop\n  Makefile: wire up the clar unit testing framework\n  t/unit-tests: convert strvec tests to use clar\n  t/unit-tests: convert ctype tests to use clar\n\n .gitignore                                 |   1 +\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |  42 +-\n t/Makefile                                 |   4 +-\n t/run-test.sh                              |   2 +-\n t/unit-tests/.gitignore                    |   2 +\n t/unit-tests/clar-generate.awk             |  50 ++\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n t/unit-tests/{t-strvec.c => strvec.c}      | 119 ++-\n t/unit-tests/unit-test.c                   |  17 +\n t/unit-tests/unit-test.h                   |   3 +\n 29 files changed, 3159 insertions(+), 98 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\nRange-diff against v2:\n1:  78a9cc1162 = 1:  78a9cc1162 t: do not pass GIT_TEST_OPTS to unit tests with prove\n2:  6a88cf22a5 ! 2:  b6c066ee4e t: import the clar unit testing framework\n    @@ Documentation/technical/unit-tests.txt: Framework,\"<<license,License>>\",\"<<vendo\n      {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n      {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n      {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n    -+{clar},{lgpl},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n    ++{clar},{isc},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n      |=====\n      \n      === Additional framework candidates\n3:  a52ee59bf4 = 3:  35682b7686 t/clar: fix whitespace errors\n4:  02fb86dfbc = 4:  7a76c21bcb t/clar: fix compatibility with NonStop\n5:  848dc673c4 ! 5:  68b3c65951 Makefile: wire up the clar unit testing framework\n    @@ Makefile: endif\n      \n      bin-wrappers/%: wrap-for-bin.sh\n      \t$(call mkdir_p_parent_template)\n    -@@ Makefile: $(SP_OBJ): %.sp: %.c %.o\n    - .PHONY: sparse\n    - sparse: $(SP_OBJ)\n    - \n    --EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n    -+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/unit-test.h $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n    - ifndef OPENSSL_SHA1\n    - \tEXCEPT_HDRS += sha1/openssl.h\n    - endif\n     @@ Makefile: endif\n      \n      artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n    @@ Makefile: $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n     +$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n     +\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n     +$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n    -+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR) -I$(UNIT_TEST_DIR)/clar\n    ++$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR)\n     +$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n     +\t$(call mkdir_p_parent_template)\n     +\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n    @@ t/unit-tests/unit-test.c (new)\n      ## t/unit-tests/unit-test.h (new) ##\n     @@\n     +#include \"git-compat-util.h\"\n    -+#include \"clar.h\"\n    ++#include \"clar/clar.h\"\n     +#include \"clar-decls.h\"\n6:  578e657269 ! 6:  4a0888380e t/unit-tests: convert strvec tests to use clar\n    @@ t/unit-tests/t-strvec.c => t/unit-tests/strvec.c\n     -\tcheck_uint(vec.nr, ==, 0);\n     -\tcheck_uint(vec.alloc, ==, 0);\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n    -+\tcl_assert(vec.nr == 0);\n    -+\tcl_assert(vec.alloc == 0);\n    ++\tcl_assert_equal_i(vec.nr, 0);\n    ++\tcl_assert_equal_i(vec.alloc, 0);\n      }\n      \n     -static void t_dynamic_init(void)\n    @@ t/unit-tests/t-strvec.c => t/unit-tests/strvec.c\n     -\tcheck_uint(vec.nr, ==, 0);\n     -\tcheck_uint(vec.alloc, ==, 0);\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n    -+\tcl_assert(vec.nr == 0);\n    -+\tcl_assert(vec.alloc == 0);\n    ++\tcl_assert_equal_i(vec.nr, 0);\n    ++\tcl_assert_equal_i(vec.alloc, 0);\n      }\n      \n     -static void t_clear(void)\n    @@ t/unit-tests/t-strvec.c => t/unit-tests/strvec.c\n     -\tcheck_uint(vec.nr, ==, 0);\n     -\tcheck_uint(vec.alloc, ==, 0);\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n    -+\tcl_assert(vec.nr == 0);\n    -+\tcl_assert(vec.alloc == 0);\n    ++\tcl_assert_equal_i(vec.nr, 0);\n    ++\tcl_assert_equal_i(vec.alloc, 0);\n      }\n      \n     -static void t_push(void)\n    @@ t/unit-tests/strvec.c: static void t_detach(void)\n     -\tcheck_uint(vec.nr, ==, 0);\n     -\tcheck_uint(vec.alloc, ==, 0);\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n    -+\tcl_assert(vec.nr == 0);\n    -+\tcl_assert(vec.alloc == 0);\n    ++\tcl_assert_equal_i(vec.nr, 0);\n    ++\tcl_assert_equal_i(vec.alloc, 0);\n      \n      \tfree((char *) detached[0]);\n      \tfree(detached);\n7:  238de33b93 = 7:  f423b01c05 t/unit-tests: convert ctype tests to use clar\n-- \n2.46.0.dirty\n\n"},{"id":"500394","messageId":"78a9cc116253c8c638cc430bd0a842805105648b.1723095269.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723095269.git.ps@pks.im","subject":"[RFC PATCH v3 1/7] t: do not pass GIT_TEST_OPTS to unit tests with prove","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-08T05:38:12Z","receivedAt":"2024-08-08T05:38:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When using the prove target, we append GIT_TEST_OPTS to the arguments\nthat we execute each of the tests with. This doesn't only include the\nintended test scripts, but also ends up passing the arguments to our\nunit tests. This is unintentional though as they do not even know to\ninterpret those arguments, and is inconsistent with how we execute unit\ntests without prove.\n\nThis isn't much of an issue because our current set of unit tests mostly\nignore their arguments anyway. With the introduction of clar-based unit\ntests this is about to become an issue though, as these do parse their\ncommand line argument to alter behaviour.\n\nPrepare for this by passing GIT_TEST_OPTS to \"run-test.sh\" via an\nenvironment variable. Like this, we can conditionally forward it to our\ntest scripts, only.\n---\n t/Makefile    | 3 ++-\n t/run-test.sh | 2 +-\n 2 files changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/Makefile b/t/Makefile\nindex 4c30e7c06f..d2212de0b7 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -68,7 +68,8 @@ failed:\n \ttest -z \"$$failed\" || $(MAKE) $$failed\n \n prove: pre-clean check-chainlint $(TEST_LINT)\n-\t@echo \"*** prove (shell & unit tests) ***\"; $(CHAINLINTSUPPRESS) TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS) :: $(GIT_TEST_OPTS)\n+\t@echo \"*** prove (shell & unit tests) ***\"\n+\t@$(CHAINLINTSUPPRESS) TEST_OPTIONS='$(GIT_TEST_OPTS)' TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS)\n \t$(MAKE) clean-except-prove-cache\n \n $(T):\ndiff --git a/t/run-test.sh b/t/run-test.sh\nindex 13c353b91b..63328ac630 100755\n--- a/t/run-test.sh\n+++ b/t/run-test.sh\n@@ -10,7 +10,7 @@ case \"$1\" in\n \t\techo >&2 \"ERROR: TEST_SHELL_PATH is empty or not set\"\n \t\texit 1\n \tfi\n-\texec \"${TEST_SHELL_PATH}\" \"$@\"\n+\texec \"${TEST_SHELL_PATH}\" \"$@\" ${TEST_OPTIONS}\n \t;;\n *)\n \texec \"$@\"\n-- \n2.46.0.dirty\n\n"},{"id":"500395","messageId":"b6c066ee4e437d8b0c807a7c2ff3884856044445.1723095269.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723095269.git.ps@pks.im","subject":"[RFC PATCH v3 2/7] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-08T05:38:17Z","receivedAt":"2024-08-08T05:38:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Import the clar unit testing framework at commit faa8419 (Merge pull\nrequest #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |   4 +-\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 154 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 267 +++++++\n t/unit-tests/clar/test/.gitignore          |   5 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n 20 files changed, 2946 insertions(+), 1 deletion(-)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n\ndiff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt\nindex 206037ffb1..5a432b7b29 100644\n--- a/Documentation/technical/unit-tests.txt\n+++ b/Documentation/technical/unit-tests.txt\n@@ -203,6 +203,7 @@ GitHub / GitLab stars to estimate this.\n :criterion: https://github.com/Snaipe/Criterion[Criterion]\n :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n :check: https://libcheck.github.io/check/[Check]\n+:clar: https://github.com/clar-test/clar[Clar]\n \n [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n |=====\n@@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n+{clar},{isc},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n |=====\n \n === Additional framework candidates\ndiff --git a/Makefile b/Makefile\nindex 3863e60b66..674b0ac4e1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1331,6 +1331,8 @@ THIRD_PARTY_SOURCES += compat/poll/%\n THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n@@ -3260,7 +3262,7 @@ $(SP_OBJ): %.sp: %.c %.o\n .PHONY: sparse\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n ifndef OPENSSL_SHA1\n \tEXCEPT_HDRS += sha1/openssl.h\n endif\ndiff --git a/t/unit-tests/clar/.github/workflows/ci.yml b/t/unit-tests/clar/.github/workflows/ci.yml\nnew file mode 100644\nindex 0000000000..b1ac2de460\n--- /dev/null\n+++ b/t/unit-tests/clar/.github/workflows/ci.yml\n@@ -0,0 +1,23 @@\n+name: CI\n+\n+on:\n+  push:\n+    branches: [ main ]\n+  pull_request:\n+    branches: [ main ]\n+\n+jobs:\n+  build:\n+    strategy:\n+      matrix:\n+        os: [ ubuntu-latest, macos-latest ]\n+\n+    runs-on: ${{ matrix.os }}\n+\n+    steps:\n+    - name: Check out\n+      uses: actions/checkout@v2\n+    - name: Build\n+      run: |\n+        cd test\n+        make\ndiff --git a/t/unit-tests/clar/COPYING b/t/unit-tests/clar/COPYING\nnew file mode 100644\nindex 0000000000..8983817f0c\n--- /dev/null\n+++ b/t/unit-tests/clar/COPYING\n@@ -0,0 +1,15 @@\n+ISC License\n+\n+Copyright (c) 2011-2015 Vicent Marti\n+\n+Permission to use, copy, modify, and/or distribute this software for any\n+purpose with or without fee is hereby granted, provided that the above\n+copyright notice and this permission notice appear in all copies.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ndiff --git a/t/unit-tests/clar/README.md b/t/unit-tests/clar/README.md\nnew file mode 100644\nindex 0000000000..a8961c5f10\n--- /dev/null\n+++ b/t/unit-tests/clar/README.md\n@@ -0,0 +1,329 @@\n+Come out and Clar\n+=================\n+\n+In Catalan, \"clar\" means clear, easy to perceive.  Using clar will make it\n+easy to test and make clear the quality of your code.\n+\n+> _Historical note_\n+>\n+> Originally the clar project was named \"clay\" because the word \"test\" has its\n+> roots in the latin word *\"testum\"*, meaning \"earthen pot\", and *\"testa\"*,\n+> meaning \"piece of burned clay\"?\n+>\n+> This is because historically, testing implied melting metal in a pot to\n+> check its quality.  Clay is what tests are made of.\n+\n+## Quick Usage Overview\n+\n+Clar is a minimal C unit testing framework. It's been written to replace the\n+old framework in [libgit2][libgit2], but it's both very versatile and\n+straightforward to use.\n+\n+Can you count to funk?\n+\n+- **Zero: Initialize test directory**\n+\n+    ~~~~ sh\n+    $ mkdir tests\n+    $ cp -r $CLAR_ROOT/clar* tests\n+    $ cp $CLAR_ROOT/test/clar_test.h tests\n+    $ cp $CLAR_ROOT/test/main.c.sample tests/main.c\n+    ~~~~\n+\n+- **One: Write some tests**\n+\n+    File: tests/adding.c:\n+\n+    ~~~~ c\n+    /* adding.c for the \"Adding\" suite */\n+    #include \"clar.h\"\n+\n+    static int *answer;\n+\n+    void test_adding__initialize(void)\n+    {\n+        answer = malloc(sizeof(int));\n+        cl_assert_(answer != NULL, \"No memory left?\");\n+        *answer = 42;\n+    }\n+\n+    void test_adding__cleanup(void)\n+    {\n+        free(answer);\n+    }\n+\n+    void test_adding__make_sure_math_still_works(void)\n+    {\n+        cl_assert_(5 > 3, \"Five should probably be greater than three\");\n+        cl_assert_(-5 < 2, \"Negative numbers are small, I think\");\n+        cl_assert_(*answer == 42, \"The universe is doing OK. And the initializer too.\");\n+    }\n+    ~~~~~\n+\n+- **Two: Build the test executable**\n+\n+    ~~~~ sh\n+    $ cd tests\n+    $ $CLAR_PATH/generate.py .\n+    Written `clar.suite` (1 suites)\n+    $ gcc -I. clar.c main.c adding.c -o testit\n+    ~~~~\n+\n+- **Funk: Funk it.**\n+\n+    ~~~~ sh\n+    $ ./testit\n+    ~~~~\n+\n+## The Clar Test Suite\n+\n+Writing a test suite is pretty straightforward. Each test suite is a `*.c`\n+file with a descriptive name: this encourages modularity.\n+\n+Each test suite has optional initialize and cleanup methods. These methods\n+will be called before and after running **each** test in the suite, even if\n+such test fails. As a rule of thumb, if a test needs a different initializer\n+or cleanup method than another test in the same module, that means it\n+doesn't belong in that module. Keep that in mind when grouping tests\n+together.\n+\n+The `initialize` and `cleanup` methods have the following syntax, with\n+`suitename` being the current suite name, e.g. `adding` for the `adding.c`\n+suite.\n+\n+~~~~ c\n+void test_suitename__initialize(void)\n+{\n+    /* init */\n+}\n+\n+void test_suitename__cleanup(void)\n+{\n+    /* cleanup */\n+}\n+~~~~\n+\n+These methods are encouraged to use static, global variables to store the state\n+that will be used by all tests inside the suite.\n+\n+~~~~ c\n+static git_repository *_repository;\n+\n+void test_status__initialize(void)\n+{\n+    create_tmp_repo(STATUS_REPO);\n+    git_repository_open(_repository, STATUS_REPO);\n+}\n+\n+void test_status__cleanup(void)\n+{\n+    git_repository_close(_repository);\n+    git_path_rm(STATUS_REPO);\n+}\n+\n+void test_status__simple_test(void)\n+{\n+    /* do something with _repository */\n+}\n+~~~~\n+\n+Writing the actual tests is just as straightforward. Tests have the\n+`void test_suitename__test_name(void)` signature, and they should **not**\n+be static. Clar will automatically detect and list them.\n+\n+Tests are run as they appear on their original suites: they have no return\n+value. A test is considered \"passed\" if it doesn't raise any errors. Check\n+the \"Clar API\" section to see the various helper functions to check and\n+raise errors during test execution.\n+\n+__Caution:__ If you use assertions inside of `test_suitename__initialize`,\n+make sure that you do not rely on `__initialize` being completely run\n+inside your `test_suitename__cleanup` function. Otherwise you might\n+encounter ressource cleanup twice.\n+\n+## How does Clar work?\n+\n+To use Clar:\n+\n+1. copy the Clar boilerplate to your test directory\n+2. copy (and probably modify) the sample `main.c` (from\n+   `$CLAR_PATH/test/main.c.sample`)\n+3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and\n+   write out the test suite metadata.\n+4. compile your test files and the Clar boilerplate into a single test\n+   executable\n+5. run the executable to test!\n+\n+The Clar boilerplate gives you a set of useful test assertions and features\n+(like accessing or making sandbox copies of fixture data).  It consists of\n+the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory.\n+You should not need to edit these files.\n+\n+The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes\n+`clar_test(argc, argv)` to run the tests.  Usually, you will edit this file\n+to perform any framework specific initialization and teardown that you need.\n+\n+The Clar mixer (`generate.py`) recursively scans your test directory for\n+any `.c` files, parses them, and writes the `clar.suite` file with all of\n+the metadata about your tests.  When you build, the `clar.suite` file is\n+included into `clar.c`.\n+\n+The mixer can be run with **Python 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and PyPy 1.6**.\n+\n+Commandline usage of the mixer is as follows:\n+\n+    $ ./generate.py .\n+\n+Where `.` is the folder where all the test suites can be found. The mixer\n+will automatically locate all the relevant source files and build the\n+testing metadata. The metadata will be written to `clar.suite`, in the same\n+folder as all the test suites. This file is included by `clar.c` and so\n+must be accessible via `#include` when building the test executable.\n+\n+    $ gcc -I. clar.c main.c suite1.c test2.c -o run_tests\n+\n+**Note that the Clar mixer only needs to be ran when adding new tests to a\n+suite, in order to regenerate the metadata**. As a result, the `clar.suite`\n+file can be checked into version control if you wish to be able to build\n+your test suite without having to re-run the mixer.\n+\n+This is handy when e.g. generating tests in a local computer, and then\n+building and testing them on an embedded device or a platform where Python\n+is not available.\n+\n+### Fixtures\n+\n+Clar can create sandboxed fixtures for you to use in your test. You'll need to compile *clar.c* with an additional `CFLAG`, `-DCLAR_FIXTURE_PATH`. This should be an absolute path to your fixtures directory.\n+\n+Once that's done, you can use the fixture API as defined below.\n+\n+## The Clar API\n+\n+Clar makes the following methods available from all functions in a test\n+suite.\n+\n+-   `cl_must_pass(call)`, `cl_must_pass_(call, message)`: Verify that the given\n+    function call passes, in the POSIX sense (returns a value greater or equal\n+    to 0).\n+\n+-   `cl_must_fail(call)`, `cl_must_fail_(call, message)`: Verify that the given\n+    function call fails, in the POSIX sense (returns a value less than 0).\n+\n+-   `cl_assert(expr)`, `cl_assert_(expr, message)`: Verify that `expr` is true.\n+\n+-   `cl_check_pass(call)`, `cl_check_pass_(call, message)`: Verify that the\n+    given function call passes, in the POSIX sense (returns a value greater or\n+    equal to 0). If the function call doesn't succeed, a test failure will be\n+    logged but the test's execution will continue.\n+\n+-   `cl_check_fail(call)`, `cl_check_fail_(call, message)`: Verify that the\n+    given function call fails, in the POSIX sense (returns a value less than\n+    0). If the function call doesn't fail, a test failure will be logged but\n+    the test's execution will continue.\n+\n+-   `cl_check(expr)`: Verify that `expr` is true. If `expr` is not\n+    true, a test failure will be logged but the test's execution will continue.\n+\n+-   `cl_fail(message)`: Fail the current test with the given message.\n+\n+-   `cl_warning(message)`: Issue a warning. This warning will be\n+    logged as a test failure but the test's execution will continue.\n+\n+-   `cl_set_cleanup(void (*cleanup)(void *), void *opaque)`: Set the cleanup\n+    method for a single test. This method will be called with `opaque` as its\n+    argument before the test returns (even if the test has failed).\n+    If a global cleanup method is also available, the local cleanup will be\n+    called first, and then the global.\n+\n+-   `cl_assert_equal_i(int,int)`: Verify that two integer values are equal.\n+    The advantage of this over a simple `cl_assert` is that it will format\n+    a much nicer error report if the values are not equal.\n+\n+-   `cl_assert_equal_s(const char *,const char *)`: Verify that two strings\n+    are equal.  The expected value can also be NULL and this will correctly\n+    test for that.\n+\n+-   `cl_fixture_sandbox(const char *)`: Sets up a sandbox for a fixture\n+    so that you can mutate the file directly.\n+\n+-   `cl_fixture_cleanup(const char *)`: Tears down the previous fixture\n+    sandbox.\n+\n+-   `cl_fixture(const char *)`: Gets the full path to a fixture file.\n+\n+Please do note that these methods are *always* available whilst running a\n+test, even when calling auxiliary/static functions inside the same file.\n+\n+It's strongly encouraged to perform test assertions in auxiliary methods,\n+instead of returning error values. This is considered good Clar style.\n+\n+Style Example:\n+\n+~~~~ c\n+/*\n+ * Bad style: auxiliary functions return an error code\n+ */\n+\n+static int check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    if (aux == NULL)\n+        return -1;\n+\n+    return strcmp(my_function(aux), str) == 0 ? 0 : -1;\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    cl_must_pass_(\n+        check_string(\"foo\"),\n+        \"String differs after processing\"\n+    );\n+\n+    cl_must_pass_(\n+        check_string(\"bar\"),\n+        \"String differs after processing\"\n+    );\n+}\n+~~~~\n+\n+~~~~ c\n+/*\n+ * Good style: auxiliary functions perform assertions\n+ */\n+\n+static void check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    cl_assert_(\n+        aux != NULL,\n+        \"String processing failed\"\n+    );\n+\n+    cl_assert_(\n+        strcmp(my_function(aux), str) == 0,\n+        \"String differs after processing\"\n+    );\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    check_string(\"foo\");\n+    check_string(\"bar\");\n+}\n+~~~~\n+\n+About Clar\n+==========\n+\n+Clar has been written from scratch by [Vicent Martí](https://github.com/vmg),\n+to replace the old testing framework in [libgit2][libgit2].\n+\n+Do you know what languages are *in* on the SF startup scene? Node.js *and*\n+Latin.  Follow [@vmg](https://www.twitter.com/vmg) on Twitter to\n+receive more lessons on word etymology. You can be hip too.\n+\n+\n+[libgit2]: https://github.com/libgit2/libgit2\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nnew file mode 100644\nindex 0000000000..3fc2c76815\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.c\n@@ -0,0 +1,842 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#include <assert.h>\n+#include <setjmp.h>\n+#include <stdlib.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <math.h>\n+#include <stdarg.h>\n+#include <wchar.h>\n+#include <time.h>\n+\n+/* required for sandboxing */\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#ifdef _WIN32\n+#\tinclude <windows.h>\n+#\tinclude <io.h>\n+#\tinclude <shellapi.h>\n+#\tinclude <direct.h>\n+\n+#\tdefine _MAIN_CC __cdecl\n+\n+#\tifndef stat\n+#\t\tdefine stat(path, st) _stat(path, st)\n+#\tendif\n+#\tifndef mkdir\n+#\t\tdefine mkdir(path, mode) _mkdir(path)\n+#\tendif\n+#\tifndef chdir\n+#\t\tdefine chdir(path) _chdir(path)\n+#\tendif\n+#\tifndef access\n+#\t\tdefine access(path, mode) _access(path, mode)\n+#\tendif\n+#\tifndef strdup\n+#\t\tdefine strdup(str) _strdup(str)\n+#\tendif\n+#\tifndef strcasecmp\n+#\t\tdefine strcasecmp(a,b) _stricmp(a,b)\n+#\tendif\n+\n+#\tifndef __MINGW32__\n+#\t\tpragma comment(lib, \"shell32\")\n+#\t\tifndef strncpy\n+#\t\t\tdefine strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)\n+#\t\tendif\n+#\t\tifndef W_OK\n+#\t\t\tdefine W_OK 02\n+#\t\tendif\n+#\t\tifndef S_ISDIR\n+#\t\t\tdefine S_ISDIR(x) ((x & _S_IFDIR) != 0)\n+#\t\tendif\n+#\t\tdefine p_snprintf(buf,sz,fmt,...) _snprintf_s(buf,sz,_TRUNCATE,fmt,__VA_ARGS__)\n+#\telse\n+#\t\tdefine p_snprintf snprintf\n+#\tendif\n+\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"Iu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"Ix\"\n+#\tendif\n+\n+#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+\ttypedef struct stat STAT_T;\n+#\telse\n+\ttypedef struct _stat STAT_T;\n+#\tendif\n+#else\n+#\tinclude <sys/wait.h> /* waitpid(2) */\n+#\tinclude <unistd.h>\n+#\tdefine _MAIN_CC\n+#\tdefine p_snprintf snprintf\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"zu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"zx\"\n+#\tendif\n+\ttypedef struct stat STAT_T;\n+#endif\n+\n+#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n+\n+#include \"clar.h\"\n+\n+static void fs_rm(const char *_source);\n+static void fs_copy(const char *_source, const char *dest);\n+\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name);\n+#endif\n+\n+struct clar_error {\n+\tconst char *file;\n+\tconst char *function;\n+\tsize_t line_number;\n+\tconst char *error_msg;\n+\tchar *description;\n+\n+\tstruct clar_error *next;\n+};\n+\n+struct clar_explicit {\n+\tsize_t suite_idx;\n+\tconst char *filter;\n+\n+\tstruct clar_explicit *next;\n+};\n+\n+struct clar_report {\n+\tconst char *test;\n+\tint test_number;\n+\tconst char *suite;\n+\n+\tenum cl_test_status status;\n+\ttime_t start;\n+\tdouble elapsed;\n+\n+\tstruct clar_error *errors;\n+\tstruct clar_error *last_error;\n+\n+\tstruct clar_report *next;\n+};\n+\n+struct clar_summary {\n+\tconst char *filename;\n+\tFILE *fp;\n+};\n+\n+static struct {\n+\tenum cl_test_status test_status;\n+\n+\tconst char *active_test;\n+\tconst char *active_suite;\n+\n+\tint total_skipped;\n+\tint total_errors;\n+\n+\tint tests_ran;\n+\tint suites_ran;\n+\n+\tenum cl_output_format output_format;\n+\n+\tint report_errors_only;\n+\tint exit_on_error;\n+\tint verbosity;\n+\n+\tint write_summary;\n+\tchar *summary_filename;\n+\tstruct clar_summary *summary;\n+\n+\tstruct clar_explicit *explicit;\n+\tstruct clar_explicit *last_explicit;\n+\n+\tstruct clar_report *reports;\n+\tstruct clar_report *last_report;\n+\n+\tvoid (*local_cleanup)(void *);\n+\tvoid *local_cleanup_payload;\n+\n+\tjmp_buf trampoline;\n+\tint trampoline_enabled;\n+\n+\tcl_trace_cb *pfn_trace_cb;\n+\tvoid *trace_payload;\n+\n+} _clar;\n+\n+struct clar_func {\n+\tconst char *name;\n+\tvoid (*ptr)(void);\n+};\n+\n+struct clar_suite {\n+\tconst char *name;\n+\tstruct clar_func initialize;\n+\tstruct clar_func cleanup;\n+\tconst struct clar_func *tests;\n+\tsize_t test_count;\n+\tint enabled;\n+};\n+\n+/* From clar_print_*.c */\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n+static void clar_print_onsuite(const char *suite_name, int suite_index);\n+static void clar_print_onabort(const char *msg, ...);\n+\n+/* From clar_sandbox.c */\n+static void clar_unsandbox(void);\n+static int clar_sandbox(void);\n+\n+/* From summary.h */\n+static struct clar_summary *clar_summary_init(const char *filename);\n+static int clar_summary_shutdown(struct clar_summary *fp);\n+\n+/* Load the declarations for the test suite */\n+#include \"clar.suite\"\n+\n+\n+#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n+\t} while (0)\n+\n+void cl_trace_register(cl_trace_cb *cb, void *payload)\n+{\n+\t_clar.pfn_trace_cb = cb;\n+\t_clar.trace_payload = payload;\n+}\n+\n+\n+/* Core test functions */\n+static void\n+clar_report_errors(struct clar_report *report)\n+{\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (error = report->errors; error; error = error->next)\n+\t\tclar_print_error(i++, _clar.last_report, error);\n+}\n+\n+static void\n+clar_report_all(void)\n+{\n+\tstruct clar_report *report;\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (report = _clar.reports; report; report = report->next) {\n+\t\tif (report->status != CL_TEST_FAILURE)\n+\t\t\tcontinue;\n+\n+\t\tfor (error = report->errors; error; error = error->next)\n+\t\t\tclar_print_error(i++, report, error);\n+\t}\n+}\n+\n+#ifdef WIN32\n+# define clar_time DWORD\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\t*out = GetTickCount();\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)*end - (double)*start) / 1000;\n+}\n+#else\n+# include <sys/time.h>\n+\n+# define clar_time struct timeval\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\tstruct timezone tz;\n+\n+\tgettimeofday(out, &tz);\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n+\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n+}\n+#endif\n+\n+static void\n+clar_run_test(\n+\tconst struct clar_suite *suite,\n+\tconst struct clar_func *test,\n+\tconst struct clar_func *initialize,\n+\tconst struct clar_func *cleanup)\n+{\n+\tclar_time start, end;\n+\n+\t_clar.trampoline_enabled = 1;\n+\n+\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n+\n+\t_clar.last_report->start = time(NULL);\n+\tclar_time_now(&start);\n+\n+\tif (setjmp(_clar.trampoline) == 0) {\n+\t\tif (initialize->ptr != NULL)\n+\t\t\tinitialize->ptr();\n+\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n+\t\ttest->ptr();\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n+\t}\n+\n+\tclar_time_now(&end);\n+\n+\t_clar.trampoline_enabled = 0;\n+\n+\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n+\t\t_clar.last_report->status = CL_TEST_OK;\n+\n+\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n+\n+\tif (_clar.local_cleanup != NULL)\n+\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n+\n+\tif (cleanup->ptr != NULL)\n+\t\tcleanup->ptr();\n+\n+\tCL_TRACE(CL_TRACE__TEST__END);\n+\n+\t_clar.tests_ran++;\n+\n+\t/* remove any local-set cleanup methods */\n+\t_clar.local_cleanup = NULL;\n+\t_clar.local_cleanup_payload = NULL;\n+\n+\tif (_clar.report_errors_only) {\n+\t\tclar_report_errors(_clar.last_report);\n+\t} else {\n+\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n+\t}\n+}\n+\n+static void\n+clar_run_suite(const struct clar_suite *suite, const char *filter)\n+{\n+\tconst struct clar_func *test = suite->tests;\n+\tsize_t i, matchlen;\n+\tstruct clar_report *report;\n+\tint exact = 0;\n+\n+\tif (!suite->enabled)\n+\t\treturn;\n+\n+\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\treturn;\n+\n+\tif (!_clar.report_errors_only)\n+\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n+\n+\t_clar.active_suite = suite->name;\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n+\n+\tif (filter) {\n+\t\tsize_t suitelen = strlen(suite->name);\n+\t\tmatchlen = strlen(filter);\n+\t\tif (matchlen <= suitelen) {\n+\t\t\tfilter = NULL;\n+\t\t} else {\n+\t\t\tfilter += suitelen;\n+\t\t\twhile (*filter == ':')\n+\t\t\t\t++filter;\n+\t\t\tmatchlen = strlen(filter);\n+\n+\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n+\t\t\t\texact = 1;\n+\t\t\t\tmatchlen--;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (i = 0; i < suite->test_count; ++i) {\n+\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n+\t\t\tcontinue;\n+\n+\t\tif (exact && strlen(test[i].name) != matchlen)\n+\t\t\tcontinue;\n+\n+\t\t_clar.active_test = test[i].name;\n+\n+\t\treport = calloc(1, sizeof(struct clar_report));\n+\t\treport->suite = _clar.active_suite;\n+\t\treport->test = _clar.active_test;\n+\t\treport->test_number = _clar.tests_ran;\n+\t\treport->status = CL_TEST_NOTRUN;\n+\n+\t\tif (_clar.reports == NULL)\n+\t\t\t_clar.reports = report;\n+\n+\t\tif (_clar.last_report != NULL)\n+\t\t\t_clar.last_report->next = report;\n+\n+\t\t_clar.last_report = report;\n+\n+\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n+\n+\t\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\t\treturn;\n+\t}\n+\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_END);\n+}\n+\n+static void\n+clar_usage(const char *arg)\n+{\n+\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n+\tprintf(\"Options:\\n\");\n+\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n+\tprintf(\"  -iname        Include the suite with `name`\\n\");\n+\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n+\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n+\tprintf(\"  -q            Only report tests that had an error\\n\");\n+\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n+\tprintf(\"  -t            Display results in tap format\\n\");\n+\tprintf(\"  -l            Print suite names\\n\");\n+\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n+\texit(-1);\n+}\n+\n+static void\n+clar_parse_args(int argc, char **argv)\n+{\n+\tint i;\n+\n+\t/* Verify options before execute */\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tif (argument[0] != '-' || argument[1] == '\\0'\n+\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n+\t\t\tclar_usage(argv[0]);\n+\t\t}\n+\t}\n+\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tswitch (argument[1]) {\n+\t\tcase 's':\n+\t\tcase 'i':\n+\t\tcase 'x': { /* given suite name */\n+\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n+\t\t\tchar action = argument[1];\n+\t\t\tsize_t j, arglen, suitelen, cmplen;\n+\n+\t\t\targument += offset;\n+\t\t\targlen = strlen(argument);\n+\n+\t\t\tif (arglen == 0)\n+\t\t\t\tclar_usage(argv[0]);\n+\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n+\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n+\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n+\n+\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n+\t\t\t\t\tint exact = (arglen >= suitelen);\n+\n+\t\t\t\t\t/* Do we have a real suite prefix separated by a\n+\t\t\t\t\t * trailing '::' or just a matching substring? */\n+\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n+\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n+\t\t\t\t\t    continue;\n+\n+\t\t\t\t\t++found;\n+\n+\t\t\t\t\tif (!exact)\n+\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n+\n+\t\t\t\t\tswitch (action) {\n+\t\t\t\t\tcase 's': {\n+\t\t\t\t\t\tstruct clar_explicit *explicit =\n+\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n+\t\t\t\t\t\tassert(explicit);\n+\n+\t\t\t\t\t\texplicit->suite_idx = j;\n+\t\t\t\t\t\texplicit->filter = argument;\n+\n+\t\t\t\t\t\tif (_clar.explicit == NULL)\n+\t\t\t\t\t\t\t_clar.explicit = explicit;\n+\n+\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n+\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n+\n+\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n+\t\t\t\t\t\t_clar.last_explicit = explicit;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n+\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif (exact)\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (!found) {\n+\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n+\t\t\t\texit(-1);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase 'q':\n+\t\t\t_clar.report_errors_only = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 'Q':\n+\t\t\t_clar.exit_on_error = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 't':\n+\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n+\t\t\tbreak;\n+\n+\t\tcase 'l': {\n+\t\t\tsize_t j;\n+\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n+\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n+\n+\t\t\texit(0);\n+\t\t}\n+\n+\t\tcase 'v':\n+\t\t\t_clar.verbosity++;\n+\t\t\tbreak;\n+\n+\t\tcase 'r':\n+\t\t\t_clar.write_summary = 1;\n+\t\t\tfree(_clar.summary_filename);\n+\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tassert(!\"Unexpected commandline argument!\");\n+\t\t}\n+\t}\n+}\n+\n+void\n+clar_test_init(int argc, char **argv)\n+{\n+\tconst char *summary_env;\n+\n+\tif (argc > 1)\n+\t\tclar_parse_args(argc, argv);\n+\n+\tclar_print_init(\n+\t\t(int)_clar_callback_count,\n+\t\t(int)_clar_suite_count,\n+\t\t\"\"\n+\t);\n+\n+\tif (!_clar.summary_filename &&\n+\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n+\t\t_clar.write_summary = 1;\n+\t\t_clar.summary_filename = strdup(summary_env);\n+\t}\n+\n+\tif (_clar.write_summary && !_clar.summary_filename)\n+\t\t_clar.summary_filename = strdup(\"summary.xml\");\n+\n+\tif (_clar.write_summary &&\n+\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n+\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tif (clar_sandbox() < 0) {\n+\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n+\t\texit(-1);\n+\t}\n+}\n+\n+int\n+clar_test_run(void)\n+{\n+\tsize_t i;\n+\tstruct clar_explicit *explicit;\n+\n+\tif (_clar.explicit) {\n+\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n+\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n+\t} else {\n+\t\tfor (i = 0; i < _clar_suite_count; ++i)\n+\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n+\t}\n+\n+\treturn _clar.total_errors;\n+}\n+\n+void\n+clar_test_shutdown(void)\n+{\n+\tstruct clar_explicit *explicit, *explicit_next;\n+\tstruct clar_report *report, *report_next;\n+\n+\tclar_print_shutdown(\n+\t\t_clar.tests_ran,\n+\t\t(int)_clar_suite_count,\n+\t\t_clar.total_errors\n+\t);\n+\n+\tclar_unsandbox();\n+\n+\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n+\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n+\t\texplicit_next = explicit->next;\n+\t\tfree(explicit);\n+\t}\n+\n+\tfor (report = _clar.reports; report; report = report_next) {\n+\t\treport_next = report->next;\n+\t\tfree(report);\n+\t}\n+\n+\tfree(_clar.summary_filename);\n+}\n+\n+int\n+clar_test(int argc, char **argv)\n+{\n+\tint errors;\n+\n+\tclar_test_init(argc, argv);\n+\terrors = clar_test_run();\n+\tclar_test_shutdown();\n+\n+\treturn errors;\n+}\n+\n+static void abort_test(void)\n+{\n+\tif (!_clar.trampoline_enabled) {\n+\t\tclar_print_onabort(\n+\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n+\t\tclar_report_errors(_clar.last_report);\n+\t\texit(-1);\n+\t}\n+\n+\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n+\tlongjmp(_clar.trampoline, -1);\n+}\n+\n+void clar__skip(void)\n+{\n+\t_clar.last_report->status = CL_TEST_SKIP;\n+\t_clar.total_skipped++;\n+\tabort_test();\n+}\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n+\n+\tif (_clar.last_report->errors == NULL)\n+\t\t_clar.last_report->errors = error;\n+\n+\tif (_clar.last_report->last_error != NULL)\n+\t\t_clar.last_report->last_error->next = error;\n+\n+\t_clar.last_report->last_error = error;\n+\n+\terror->file = file;\n+\terror->function = function;\n+\terror->line_number = line;\n+\terror->error_msg = error_msg;\n+\n+\tif (description != NULL)\n+\t\terror->description = strdup(description);\n+\n+\t_clar.total_errors++;\n+\t_clar.last_report->status = CL_TEST_FAILURE;\n+\n+\tif (should_abort)\n+\t\tabort_test();\n+}\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tif (condition)\n+\t\treturn;\n+\n+\tclar__fail(file, function, line, error_msg, description, should_abort);\n+}\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...)\n+{\n+\tva_list args;\n+\tchar buf[4096];\n+\tint is_equal = 1;\n+\n+\tva_start(args, fmt);\n+\n+\tif (!strcmp(\"%s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n+\t\t\t\t\ts1, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n+\t\t\t\t\tlen, s1, len, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n+\t\t\t\t\twcs1, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n+\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n+\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n+\t\tis_equal = (sz1 == sz2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%p\", fmt)) {\n+\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n+\t\tis_equal = (p1 == p2);\n+\t\tif (!is_equal)\n+\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n+\t}\n+\telse {\n+\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n+\t\tis_equal = (i1 == i2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n+\t\t}\n+\t}\n+\n+\tva_end(args);\n+\n+\tif (!is_equal)\n+\t\tclar__fail(file, function, line, err, buf, should_abort);\n+}\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n+{\n+\t_clar.local_cleanup = cleanup;\n+\t_clar.local_cleanup_payload = opaque;\n+}\n+\n+#include \"clar/sandbox.h\"\n+#include \"clar/fixtures.h\"\n+#include \"clar/fs.h\"\n+#include \"clar/print.h\"\n+#include \"clar/summary.h\"\ndiff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\nnew file mode 100644\nindex 0000000000..8c22382bd5\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.h\n@@ -0,0 +1,173 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST_H__\n+#define __CLAR_TEST_H__\n+\n+#include <stdlib.h>\n+\n+enum cl_test_status {\n+\tCL_TEST_OK,\n+\tCL_TEST_FAILURE,\n+\tCL_TEST_SKIP,\n+\tCL_TEST_NOTRUN,\n+};\n+\n+enum cl_output_format {\n+\tCL_OUTPUT_CLAP,\n+\tCL_OUTPUT_TAP,\n+};\n+\n+/** Setup clar environment */\n+void clar_test_init(int argc, char *argv[]);\n+int clar_test_run(void);\n+void clar_test_shutdown(void);\n+\n+/** One shot setup & run */\n+int clar_test(int argc, char *argv[]);\n+\n+const char *clar_sandbox_path(void);\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n+void cl_fs_cleanup(void);\n+\n+/**\n+ * cl_trace_* is a hook to provide a simple global tracing\n+ * mechanism.\n+ *\n+ * The goal here is to let main() provide clar-proper\n+ * with a callback to optionally write log info for\n+ * test operations into the same stream used by their\n+ * actual tests.  This would let them print test names\n+ * and maybe performance data as they choose.\n+ *\n+ * The goal is NOT to alter the flow of control or to\n+ * override test selection/skipping.  (So the callback\n+ * does not return a value.)\n+ *\n+ * The goal is NOT to duplicate the existing\n+ * pass/fail/skip reporting.  (So the callback\n+ * does not accept a status/errorcode argument.)\n+ *\n+ */\n+typedef enum cl_trace_event {\n+\tCL_TRACE__SUITE_BEGIN,\n+\tCL_TRACE__SUITE_END,\n+\tCL_TRACE__TEST__BEGIN,\n+\tCL_TRACE__TEST__END,\n+\tCL_TRACE__TEST__RUN_BEGIN,\n+\tCL_TRACE__TEST__RUN_END,\n+\tCL_TRACE__TEST__LONGJMP,\n+} cl_trace_event;\n+\n+typedef void (cl_trace_cb)(\n+\tcl_trace_event ev,\n+\tconst char *suite_name,\n+\tconst char *test_name,\n+\tvoid *payload);\n+\n+/**\n+ * Register a callback into CLAR to send global trace events.\n+ * Pass NULL to disable.\n+ */\n+void cl_trace_register(cl_trace_cb *cb, void *payload);\n+\n+\n+#ifdef CLAR_FIXTURE_PATH\n+const char *cl_fixture(const char *fixture_name);\n+void cl_fixture_sandbox(const char *fixture_name);\n+void cl_fixture_cleanup(const char *fixture_name);\n+const char *cl_fixture_basename(const char *fixture_name);\n+#endif\n+\n+/**\n+ * Assertion macros with explicit error message\n+ */\n+#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n+#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n+#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n+\n+/**\n+ * Check macros with explicit error message\n+ */\n+#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n+#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n+#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n+\n+/**\n+ * Assertion macros with no error message\n+ */\n+#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n+#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n+#define cl_assert(expr) cl_assert_(expr, NULL)\n+\n+/**\n+ * Check macros with no error message\n+ */\n+#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n+#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n+#define cl_check(expr) cl_check_(expr, NULL)\n+\n+/**\n+ * Forced failure/warning\n+ */\n+#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n+#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n+\n+#define cl_skip() clar__skip()\n+\n+/**\n+ * Typed assertion macros\n+ */\n+#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n+#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n+\n+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n+\n+#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n+#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n+\n+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+\n+#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n+#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n+#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n+\n+#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n+\n+#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n+\n+void clar__skip(void);\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...);\n+\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\nnew file mode 100644\nindex 0000000000..6ec6423484\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fixtures.h\n@@ -0,0 +1,50 @@\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name)\n+{\n+\tstatic char _path[4096];\n+\tsize_t root_len;\n+\n+\troot_len = strlen(base);\n+\tstrncpy(_path, base, sizeof(_path));\n+\n+\tif (_path[root_len - 1] != '/')\n+\t\t_path[root_len++] = '/';\n+\n+\tif (fixture_name[0] == '/')\n+\t\tfixture_name++;\n+\n+\tstrncpy(_path + root_len,\n+\t\tfixture_name,\n+\t\tsizeof(_path) - root_len);\n+\n+\treturn _path;\n+}\n+\n+const char *cl_fixture(const char *fixture_name)\n+{\n+\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n+}\n+\n+void cl_fixture_sandbox(const char *fixture_name)\n+{\n+\tfs_copy(cl_fixture(fixture_name), _clar_path);\n+}\n+\n+const char *cl_fixture_basename(const char *fixture_name)\n+{\n+\tconst char *p;\n+\n+\tfor (p = fixture_name; *p; p++) {\n+\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n+\t\t\tfixture_name = p+1;\n+\t}\n+\n+\treturn fixture_name;\n+}\n+\n+void cl_fixture_cleanup(const char *fixture_name)\n+{\n+\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nnew file mode 100644\nindex 0000000000..a6eda5e5dc\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -0,0 +1,522 @@\n+/*\n+ * By default, use a read/write loop to copy files on POSIX systems.\n+ * On Linux, use sendfile by default as it's slightly faster.  On\n+ * macOS, we avoid fcopyfile by default because it's slightly slower.\n+ */\n+#undef USE_FCOPYFILE\n+#define USE_SENDFILE 1\n+\n+#ifdef _WIN32\n+\n+#ifdef CLAR_WIN32_LONGPATHS\n+# define CLAR_MAX_PATH 4096\n+#else\n+# define CLAR_MAX_PATH MAX_PATH\n+#endif\n+\n+#define RM_RETRY_COUNT\t5\n+#define RM_RETRY_DELAY\t10\n+\n+#ifdef __MINGW32__\n+\n+/* These security-enhanced functions are not available\n+ * in MinGW, so just use the vanilla ones */\n+#define wcscpy_s(a, b, c) wcscpy((a), (c))\n+#define wcscat_s(a, b, c) wcscat((a), (c))\n+\n+#endif /* __MINGW32__ */\n+\n+static int\n+fs__dotordotdot(WCHAR *_tocheck)\n+{\n+\treturn _tocheck[0] == '.' &&\n+\t\t(_tocheck[1] == '\\0' ||\n+\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n+}\n+\n+static int\n+fs_rmdir_rmdir(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\n+\twhile (!RemoveDirectoryW(_wpath)) {\n+\t\t/* Only retry when we have retries remaining, and the\n+\t\t * error was ERROR_DIR_NOT_EMPTY. */\n+\t\tif (retries++ > RM_RETRY_COUNT ||\n+\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n+\t\t\treturn -1;\n+\n+\t\t/* Give whatever has a handle to a child item some time\n+\t\t * to release it before trying again */\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void translate_path(WCHAR *path, size_t path_size)\n+{\n+    size_t path_len, i;\n+\n+    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n+\treturn;\n+\n+    path_len = wcslen(path);\n+    cl_assert(path_size > path_len + 4);\n+\n+    for (i = path_len; i > 0; i--) {\n+\tWCHAR c = path[i - 1];\n+\n+\tif (c == L'/')\n+\t    path[i + 3] = L'\\\\';\n+\telse\n+\t    path[i + 3] = path[i - 1];\n+    }\n+\n+    path[0] = L'\\\\';\n+    path[1] = L'\\\\';\n+    path[2] = L'?';\n+    path[3] = L'\\\\';\n+    path[path_len + 4] = L'\\0';\n+}\n+\n+static void\n+fs_rmdir_helper(WCHAR *_wsource)\n+{\n+\tWCHAR buffer[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buffer_prefix_len;\n+\n+\t/* Set up the buffer and capture the length */\n+\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n+\ttranslate_path(buffer, CLAR_MAX_PATH);\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n+\tbuffer_prefix_len = wcslen(buffer);\n+\n+\t/* FindFirstFile needs a wildcard to match multiple items */\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buffer, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_rmdir_helper(buffer);\n+\t\telse {\n+\t\t\t/* If set, the +R bit must be cleared before deleting */\n+\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n+\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\t\tcl_assert(DeleteFileW(buffer));\n+\t\t}\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+\n+\t/* Now that the directory is empty, remove it */\n+\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n+}\n+\n+static int\n+fs_rm_wait(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\tDWORD last_error;\n+\n+\tdo {\n+\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n+\t\t\tlast_error = GetLastError();\n+\t\telse\n+\t\t\tlast_error = ERROR_SUCCESS;\n+\n+\t\t/* Is the item gone? */\n+\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n+\t\t\tERROR_PATH_NOT_FOUND == last_error)\n+\t\t\treturn 0;\n+\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n+\t}\n+\twhile (retries++ <= RM_RETRY_COUNT);\n+\n+\treturn -1;\n+}\n+\n+static void\n+fs_rm(const char *_source)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH];\n+\tDWORD attrs;\n+\n+\t/* The input path is UTF-8. Convert it to wide characters\n+\t * for use with the Windows API */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1, /* Indicates NULL termination */\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\n+\t/* Does the item exist? If not, we have no work to do */\n+\tattrs = GetFileAttributesW(wsource);\n+\n+\tif (INVALID_FILE_ATTRIBUTES == attrs)\n+\t\treturn;\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n+\t\tfs_rmdir_helper(wsource);\n+\telse {\n+\t\t/* The item is a file. Strip the +R bit */\n+\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n+\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\tcl_assert(DeleteFileW(wsource));\n+\t}\n+\n+\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n+\tcl_assert(0 == fs_rm_wait(wsource));\n+}\n+\n+static void\n+fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n+{\n+\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n+\n+\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_source, CLAR_MAX_PATH);\n+\tbuf_source_prefix_len = wcslen(buf_source);\n+\n+\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n+\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n+\tbuf_dest_prefix_len = wcslen(buf_dest);\n+\n+\t/* Get an enumerator for the items in the source. */\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buf_source, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\t/* Create the target directory. */\n+\tcl_assert(CreateDirectoryW(_wdest, NULL));\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n+\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_copydir_helper(buf_source, buf_dest);\n+\t\telse\n+\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+}\n+\n+static void\n+fs_copy(const char *_source, const char *_dest)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n+\tDWORD source_attrs, dest_attrs;\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\n+\t/* The input paths are UTF-8. Convert them to wide characters\n+\t * for use with the Windows API. */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1,\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_dest,\n+\t\t\t\t-1,\n+\t\t\t\twdest,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\ttranslate_path(wdest, CLAR_MAX_PATH);\n+\n+\t/* Check the source for existence */\n+\tsource_attrs = GetFileAttributesW(wsource);\n+\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n+\n+\t/* Check the target for existence */\n+\tdest_attrs = GetFileAttributesW(wdest);\n+\n+\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n+\t\t/* Target exists; append last path part of source to target.\n+\t\t * Use FindFirstFile to parse the path */\n+\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n+\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n+\t\tFindClose(find_handle);\n+\n+\t\t/* Check the new target for existence */\n+\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n+\t}\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n+\t\tfs_copydir_helper(wsource, wdest);\n+\telse\n+\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+#ifdef CLAR_FIXTURE_PATH\n+\tfs_rm(fixture_path(_clar_path, \"*\"));\n+#endif\n+}\n+\n+#else\n+\n+#include <errno.h>\n+#include <string.h>\n+#include <limits.h>\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#if defined(__linux__)\n+# include <sys/sendfile.h>\n+#endif\n+\n+#if defined(__APPLE__)\n+# include <copyfile.h>\n+#endif\n+\n+static void basename_r(const char **out, int *out_len, const char *in)\n+{\n+\tsize_t in_len = strlen(in), start_pos;\n+\n+\tfor (in_len = strlen(in); in_len; in_len--) {\n+\t\tif (in[in_len - 1] != '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tfor (start_pos = in_len; start_pos; start_pos--) {\n+\t\tif (in[start_pos - 1] == '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tcl_assert(in_len - start_pos < INT_MAX);\n+\n+\tif (in_len - start_pos > 0) {\n+\t\t*out = &in[start_pos];\n+\t\t*out_len = (in_len - start_pos);\n+\t} else {\n+\t\t*out = \"/\";\n+\t\t*out_len = 1;\n+\t}\n+}\n+\n+static char *joinpath(const char *dir, const char *base, int base_len)\n+{\n+\tchar *out;\n+\tint len;\n+\n+\tif (base_len == -1) {\n+\t\tsize_t bl = strlen(base);\n+\n+\t\tcl_assert(bl < INT_MAX);\n+\t\tbase_len = (int)bl;\n+\t}\n+\n+\tlen = strlen(dir) + base_len + 2;\n+\tcl_assert(len > 0);\n+\n+\tcl_assert(out = malloc(len));\n+\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n+\n+\treturn out;\n+}\n+\n+static void\n+fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n+{\n+\tDIR *source_dir;\n+\tstruct dirent *d;\n+\n+\tmkdir(dest, dest_mode);\n+\n+\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n+\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(source, d->d_name, -1);\n+\t\tfs_copy(child, dest);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\n+\tclosedir(source_dir);\n+}\n+\n+static void\n+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n+{\n+\tint in, out;\n+\n+\tcl_must_pass((in = open(source, O_RDONLY)));\n+\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n+\n+#if USE_FCOPYFILE && defined(__APPLE__)\n+\t((void)(source_len)); /* unused */\n+\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n+#elif USE_SENDFILE && defined(__linux__)\n+\t{\n+\t\tssize_t ret = 0;\n+\n+\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n+\t\t\tsource_len -= (size_t)ret;\n+\t\t}\n+\t\tcl_assert(ret >= 0);\n+\t}\n+#else\n+\t{\n+\t\tchar buf[131072];\n+\t\tssize_t ret;\n+\n+\t\t((void)(source_len)); /* unused */\n+\n+\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n+\t\t\tsize_t len = (size_t)ret;\n+\n+\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n+\t\t\t\tcl_assert(ret <= (ssize_t)len);\n+\t\t\t\tlen -= ret;\n+\t\t\t}\n+\t\t\tcl_assert(ret >= 0);\n+\t\t}\n+\t\tcl_assert(ret == 0);\n+\t}\n+#endif\n+\n+\tclose(in);\n+\tclose(out);\n+}\n+\n+static void\n+fs_copy(const char *source, const char *_dest)\n+{\n+\tchar *dbuf = NULL;\n+\tconst char *dest = NULL;\n+\tstruct stat source_st, dest_st;\n+\n+\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n+\n+\tif (lstat(_dest, &dest_st) == 0) {\n+\t\tconst char *base;\n+\t\tint base_len;\n+\n+\t\t/* Target exists and is directory; append basename */\n+\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n+\n+\t\tbasename_r(&base, &base_len, source);\n+\t\tcl_assert(base_len < INT_MAX);\n+\n+\t\tdbuf = joinpath(_dest, base, base_len);\n+\t\tdest = dbuf;\n+\t} else if (errno != ENOENT) {\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t} else {\n+\t\tdest = _dest;\n+\t}\n+\n+\tif (S_ISDIR(source_st.st_mode)) {\n+\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n+\t} else {\n+\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n+\t}\n+\n+\tfree(dbuf);\n+}\n+\n+static void\n+fs_rmdir_helper(const char *path)\n+{\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n+\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(path, d->d_name, -1);\n+\t\tfs_rm(child);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\tclosedir(dir);\n+\n+\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n+}\n+\n+static void\n+fs_rm(const char *path)\n+{\n+\tstruct stat st;\n+\n+\tif (lstat(path, &st)) {\n+\t\tif (errno == ENOENT)\n+\t\t\treturn;\n+\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t}\n+\n+\tif (S_ISDIR(st.st_mode)) {\n+\t\tfs_rmdir_helper(path);\n+\t} else {\n+\t\tcl_must_pass(unlink(path));\n+\t}\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+\tclar_unsandbox();\n+\tclar_sandbox();\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\nnew file mode 100644\nindex 0000000000..c17e2f693b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/print.h\n@@ -0,0 +1,211 @@\n+/* clap: clar protocol, the traditional clar output format */\n+\n+static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n+\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n+}\n+\n+static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"\\n\\n\");\n+\tclar_report_all();\n+}\n+\n+static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tprintf(\"  %d) Failure:\\n\", num);\n+\n+\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n+\t\treport->suite,\n+\t\treport->test,\n+\t\terror->file,\n+\t\terror->line_number);\n+\n+\tprintf(\"  %s\\n\", error->error_msg);\n+\n+\tif (error->description != NULL)\n+\t\tprintf(\"  %s\\n\", error->description);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tif (_clar.verbosity > 1) {\n+\t\tprintf(\"%s::%s: \", suite_name, test_name);\n+\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n+\t\t}\n+\t} else {\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\".\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n+\t\t}\n+\n+\t\tfflush(stdout);\n+\t}\n+}\n+\n+static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tif (_clar.verbosity == 1)\n+\t\tprintf(\"\\n%s\", suite_name);\n+\n+\t(void)suite_index;\n+}\n+\n+static void clar_print_clap_onabort(const char *fmt, va_list arg)\n+{\n+\tvfprintf(stderr, fmt, arg);\n+}\n+\n+/* tap: test anywhere protocol format */\n+\n+static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)suite_names;\n+\tprintf(\"TAP version 13\\n\");\n+}\n+\n+static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"1..%d\\n\", test_count);\n+}\n+\n+static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\t(void)num;\n+\t(void)report;\n+\t(void)error;\n+}\n+\n+static void print_escaped(const char *str)\n+{\n+\tchar *c;\n+\n+\twhile ((c = strchr(str, '\\'')) != NULL) {\n+\t\tprintf(\"%.*s\", (int)(c - str), str);\n+\t\tprintf(\"''\");\n+\t\tstr = c + 1;\n+\t}\n+\n+\tprintf(\"%s\", str);\n+}\n+\n+static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tconst struct clar_error *error = _clar.last_report->errors;\n+\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tswitch(status) {\n+\tcase CL_TEST_OK:\n+\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\tcase CL_TEST_FAILURE:\n+\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\n+\t\tprintf(\"    ---\\n\");\n+\t\tprintf(\"    reason: |\\n\");\n+\t\tprintf(\"      %s\\n\", error->error_msg);\n+\n+\t\tif (error->description)\n+\t\t\tprintf(\"      %s\\n\", error->description);\n+\n+\t\tprintf(\"    at:\\n\");\n+\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n+\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n+\t\tprintf(\"      function: '%s'\\n\", error->function);\n+\t\tprintf(\"    ---\\n\");\n+\n+\t\tbreak;\n+\tcase CL_TEST_SKIP:\n+\tcase CL_TEST_NOTRUN:\n+\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\t}\n+\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n+}\n+\n+static void clar_print_tap_onabort(const char *fmt, va_list arg)\n+{\n+\tprintf(\"Bail out! \");\n+\tvprintf(fmt, arg);\n+\tfflush(stdout);\n+}\n+\n+/* indirection between protocol output selection */\n+\n+#define PRINT(FN, ...) do { \\\n+\t\tswitch (_clar.output_format) { \\\n+\t\t\tcase CL_OUTPUT_CLAP: \\\n+\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tcase CL_OUTPUT_TAP: \\\n+\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tdefault: \\\n+\t\t\t\tabort(); \\\n+\t\t} \\\n+\t} while (0)\n+\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\tPRINT(init, test_count, suite_count, suite_names);\n+}\n+\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\tPRINT(shutdown, test_count, suite_count, error_count);\n+}\n+\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tPRINT(error, num, report, error);\n+}\n+\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tPRINT(ontest, suite_name, test_name, test_number, status);\n+}\n+\n+static void clar_print_onsuite(const char *suite_name, int suite_index)\n+{\n+\tPRINT(onsuite, suite_name, suite_index);\n+}\n+\n+static void clar_print_onabort(const char *msg, ...)\n+{\n+\tva_list argp;\n+\tva_start(argp, msg);\n+\tPRINT(onabort, msg, argp);\n+\tva_end(argp);\n+}\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nnew file mode 100644\nindex 0000000000..0ba1479620\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -0,0 +1,154 @@\n+#ifdef __APPLE__\n+#include <sys/syslimits.h>\n+#endif\n+\n+static char _clar_path[4096 + 1];\n+\n+static int\n+is_valid_tmp_path(const char *path)\n+{\n+\tSTAT_T st;\n+\n+\tif (stat(path, &st) != 0)\n+\t\treturn 0;\n+\n+\tif (!S_ISDIR(st.st_mode))\n+\t\treturn 0;\n+\n+\treturn (access(path, W_OK) == 0);\n+}\n+\n+static int\n+find_tmp_path(char *buffer, size_t length)\n+{\n+#ifndef _WIN32\n+\tstatic const size_t var_count = 5;\n+\tstatic const char *env_vars[] = {\n+\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n+ \t};\n+\n+ \tsize_t i;\n+\n+\tfor (i = 0; i < var_count; ++i) {\n+\t\tconst char *env = getenv(env_vars[i]);\n+\t\tif (!env)\n+\t\t\tcontinue;\n+\n+\t\tif (is_valid_tmp_path(env)) {\n+#ifdef __APPLE__\n+\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n+\t\t\t\treturn 0;\n+#endif\n+\t\t\tstrncpy(buffer, env, length - 1);\n+\t\t\tbuffer[length - 1] = '\\0';\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\t/* If the environment doesn't say anything, try to use /tmp */\n+\tif (is_valid_tmp_path(\"/tmp\")) {\n+#ifdef __APPLE__\n+\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n+\t\t\treturn 0;\n+#endif\n+\t\tstrncpy(buffer, \"/tmp\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+#else\n+\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n+\tif (env_len > 0 && env_len < (DWORD)length)\n+\t\treturn 0;\n+\n+\tif (GetTempPath((DWORD)length, buffer))\n+\t\treturn 0;\n+#endif\n+\n+\t/* This system doesn't like us, try to use the current directory */\n+\tif (is_valid_tmp_path(\".\")) {\n+\t\tstrncpy(buffer, \".\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static void clar_unsandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0')\n+\t\treturn;\n+\n+\tcl_must_pass(chdir(\"..\"));\n+\n+\tfs_rm(_clar_path);\n+}\n+\n+static int build_sandbox_path(void)\n+{\n+#ifdef CLAR_TMPDIR\n+\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n+#else\n+\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n+#endif\n+\n+\tsize_t len;\n+\n+\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n+\t\treturn -1;\n+\n+\tlen = strlen(_clar_path);\n+\n+#ifdef _WIN32\n+\t{ /* normalize path to POSIX forward slashes */\n+\t\tsize_t i;\n+\t\tfor (i = 0; i < len; ++i) {\n+\t\t\tif (_clar_path[i] == '\\\\')\n+\t\t\t\t_clar_path[i] = '/';\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (_clar_path[len - 1] != '/') {\n+\t\t_clar_path[len++] = '/';\n+\t}\n+\n+\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n+\n+#if defined(__MINGW32__)\n+\tif (_mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(_WIN32)\n+\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#else\n+\tif (mkdtemp(_clar_path) == NULL)\n+\t\treturn -1;\n+#endif\n+\n+\treturn 0;\n+}\n+\n+static int clar_sandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n+\t\treturn -1;\n+\n+\tif (chdir(_clar_path) != 0)\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const char *clar_sandbox_path(void)\n+{\n+\treturn _clar_path;\n+}\n+\ndiff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\nnew file mode 100644\nindex 0000000000..4dd352e28b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/summary.h\n@@ -0,0 +1,143 @@\n+\n+#include <stdio.h>\n+#include <time.h>\n+\n+static int clar_summary_close_tag(\n+    struct clar_summary *summary, const char *tag, int indent)\n+{\n+\tconst char *indt;\n+\n+\tif (indent == 0) indt = \"\";\n+\telse if (indent == 1) indt = \"\\t\";\n+\telse indt = \"\\t\\t\";\n+\n+\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n+}\n+\n+static int clar_summary_testsuites(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n+}\n+\n+static int clar_summary_testsuite(struct clar_summary *summary,\n+    int idn, const char *name, time_t timestamp,\n+    int test_count, int fail_count, int error_count)\n+{\n+\tstruct tm *tm = localtime(&timestamp);\n+\tchar iso_dt[20];\n+\n+\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n+\t\treturn -1;\n+\n+\treturn fprintf(summary->fp, \"\\t<testsuite\"\n+\t\t       \" id=\\\"%d\\\"\"\n+\t\t       \" name=\\\"%s\\\"\"\n+\t\t       \" hostname=\\\"localhost\\\"\"\n+\t\t       \" timestamp=\\\"%s\\\"\"\n+\t\t       \" tests=\\\"%d\\\"\"\n+\t\t       \" failures=\\\"%d\\\"\"\n+\t\t       \" errors=\\\"%d\\\">\\n\",\n+\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n+}\n+\n+static int clar_summary_testcase(struct clar_summary *summary,\n+    const char *name, const char *classname, double elapsed)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n+\t\tname, classname, elapsed);\n+}\n+\n+static int clar_summary_failure(struct clar_summary *summary,\n+    const char *type, const char *message, const char *desc)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n+\t    type, message, desc);\n+}\n+\n+static int clar_summary_skipped(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n+}\n+\n+struct clar_summary *clar_summary_init(const char *filename)\n+{\n+\tstruct clar_summary *summary;\n+\tFILE *fp;\n+\n+\tif ((fp = fopen(filename, \"w\")) == NULL) {\n+\t\tperror(\"fopen\");\n+\t\treturn NULL;\n+\t}\n+\n+\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n+\t\tperror(\"malloc\");\n+\t\tfclose(fp);\n+\t\treturn NULL;\n+\t}\n+\n+\tsummary->filename = filename;\n+\tsummary->fp = fp;\n+\n+\treturn summary;\n+}\n+\n+int clar_summary_shutdown(struct clar_summary *summary)\n+{\n+\tstruct clar_report *report;\n+\tconst char *last_suite = NULL;\n+\n+\tif (clar_summary_testsuites(summary) < 0)\n+\t\tgoto on_error;\n+\n+\treport = _clar.reports;\n+\twhile (report != NULL) {\n+\t\tstruct clar_error *error = report->errors;\n+\n+\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n+\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\n+\t\tlast_suite = report->suite;\n+\n+\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n+\n+\t\twhile (error != NULL) {\n+\t\t\tif (clar_summary_failure(summary, \"assert\",\n+\t\t\t    error->error_msg, error->description) < 0)\n+\t\t\t\tgoto on_error;\n+\n+\t\t\terror = error->next;\n+\t\t}\n+\n+\t\tif (report->status == CL_TEST_SKIP)\n+\t\t\tclar_summary_skipped(summary);\n+\n+\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n+\t\t\tgoto on_error;\n+\n+\t\treport = report->next;\n+\n+\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\t}\n+\n+\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n+\t    fclose(summary->fp) != 0)\n+\t\tgoto on_error;\n+\n+\tprintf(\"written summary file to %s\\n\", summary->filename);\n+\n+\tfree(summary);\n+\treturn 0;\n+\n+on_error:\n+\tfclose(summary->fp);\n+\tfree(summary);\n+\treturn -1;\n+}\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nnew file mode 100755\nindex 0000000000..931b4d60d9\n--- /dev/null\n+++ b/t/unit-tests/clar/generate.py\n@@ -0,0 +1,267 @@\n+#!/usr/bin/env python\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+from __future__ import with_statement\n+from string import Template\n+import re, fnmatch, os, sys, codecs, pickle\n+\n+class Module(object):\n+    class Template(object):\n+        def __init__(self, module):\n+            self.module = module\n+\n+        def _render_callback(self, cb):\n+            if not cb:\n+                return '    { NULL, NULL }'\n+            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n+\n+    class DeclarationTemplate(Template):\n+        def render(self):\n+            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n+\n+            for initializer in self.module.initializers:\n+                out += \"extern %s;\\n\" % initializer['declaration']\n+\n+            if self.module.cleanup:\n+                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n+\n+            return out\n+\n+    class CallbacksTemplate(Template):\n+        def render(self):\n+            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n+            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n+            out += \"\\n};\\n\"\n+            return out\n+\n+    class InfoTemplate(Template):\n+        def render(self):\n+            templates = []\n+\n+            initializers = self.module.initializers\n+            if len(initializers) == 0:\n+                initializers = [ None ]\n+\n+            for initializer in initializers:\n+                name = self.module.clean_name()\n+                if initializer and initializer['short_name'].startswith('initialize_'):\n+                    variant = initializer['short_name'][len('initialize_'):]\n+                    name += \" (%s)\" % variant.replace('_', ' ')\n+\n+                template = Template(\n+            r\"\"\"\n+    {\n+        \"${clean_name}\",\n+    ${initialize},\n+    ${cleanup},\n+        ${cb_ptr}, ${cb_count}, ${enabled}\n+    }\"\"\"\n+                ).substitute(\n+                    clean_name = name,\n+                    initialize = self._render_callback(initializer),\n+                    cleanup = self._render_callback(self.module.cleanup),\n+                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n+                    cb_count = len(self.module.callbacks),\n+                    enabled = int(self.module.enabled)\n+                )\n+                templates.append(template)\n+\n+            return ','.join(templates)\n+\n+    def __init__(self, name):\n+        self.name = name\n+\n+        self.mtime = None\n+        self.enabled = True\n+        self.modified = False\n+\n+    def clean_name(self):\n+        return self.name.replace(\"_\", \"::\")\n+\n+    def _skip_comments(self, text):\n+        SKIP_COMMENTS_REGEX = re.compile(\n+            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n+            re.DOTALL | re.MULTILINE)\n+\n+        def _replacer(match):\n+            s = match.group(0)\n+            return \"\" if s.startswith('/') else s\n+\n+        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n+\n+    def parse(self, contents):\n+        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n+\n+        contents = self._skip_comments(contents)\n+        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n+\n+        self.callbacks = []\n+        self.initializers = []\n+        self.cleanup = None\n+\n+        for (declaration, symbol, short_name) in regex.findall(contents):\n+            data = {\n+                \"short_name\" : short_name,\n+                \"declaration\" : declaration,\n+                \"symbol\" : symbol\n+            }\n+\n+            if short_name.startswith('initialize'):\n+                self.initializers.append(data)\n+            elif short_name == 'cleanup':\n+                self.cleanup = data\n+            else:\n+                self.callbacks.append(data)\n+\n+        return self.callbacks != []\n+\n+    def refresh(self, path):\n+        self.modified = False\n+\n+        try:\n+            st = os.stat(path)\n+\n+            # Not modified\n+            if st.st_mtime == self.mtime:\n+                return True\n+\n+            self.modified = True\n+            self.mtime = st.st_mtime\n+\n+            with codecs.open(path, encoding='utf-8') as fp:\n+                raw_content = fp.read()\n+\n+        except IOError:\n+            return False\n+\n+        return self.parse(raw_content)\n+\n+class TestSuite(object):\n+\n+    def __init__(self, path, output):\n+        self.path = path\n+        self.output = output\n+\n+    def should_generate(self, path):\n+        if not os.path.isfile(path):\n+            return True\n+\n+        if any(module.modified for module in self.modules.values()):\n+            return True\n+\n+        return False\n+\n+    def find_modules(self):\n+        modules = []\n+        for root, _, files in os.walk(self.path):\n+            module_root = root[len(self.path):]\n+            module_root = [c for c in module_root.split(os.sep) if c]\n+\n+            tests_in_module = fnmatch.filter(files, \"*.c\")\n+\n+            for test_file in tests_in_module:\n+                full_path = os.path.join(root, test_file)\n+                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n+\n+                modules.append((full_path, module_name))\n+\n+        return modules\n+\n+    def load_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        cache = {}\n+\n+        try:\n+            fp = open(path, 'rb')\n+            cache = pickle.load(fp)\n+            fp.close()\n+        except (IOError, ValueError):\n+            pass\n+\n+        return cache\n+\n+    def save_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        with open(path, 'wb') as cache:\n+            pickle.dump(self.modules, cache)\n+\n+    def load(self, force = False):\n+        module_data = self.find_modules()\n+        self.modules = {} if force else self.load_cache()\n+\n+        for path, name in module_data:\n+            if name not in self.modules:\n+                self.modules[name] = Module(name)\n+\n+            if not self.modules[name].refresh(path):\n+                del self.modules[name]\n+\n+    def disable(self, excluded):\n+        for exclude in excluded:\n+            for module in self.modules.values():\n+                name = module.clean_name()\n+                if name.startswith(exclude):\n+                    module.enabled = False\n+                    module.modified = True\n+\n+    def suite_count(self):\n+        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n+\n+    def callback_count(self):\n+        return sum(len(module.callbacks) for module in self.modules.values())\n+\n+    def write(self):\n+        output = os.path.join(self.output, 'clar.suite')\n+\n+        if not self.should_generate(output):\n+            return False\n+\n+        with open(output, 'w') as data:\n+            modules = sorted(self.modules.values(), key=lambda module: module.name)\n+\n+            for module in modules:\n+                t = Module.DeclarationTemplate(module)\n+                data.write(t.render())\n+\n+            for module in modules:\n+                t = Module.CallbacksTemplate(module)\n+                data.write(t.render())\n+\n+            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n+                Module.InfoTemplate(module).render() for module in modules\n+            ) + \"\\n};\\n\"\n+\n+            data.write(suites)\n+\n+            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n+            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n+\n+        self.save_cache()\n+        return True\n+\n+if __name__ == '__main__':\n+    from optparse import OptionParser\n+\n+    parser = OptionParser()\n+    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n+    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n+    parser.add_option('-o', '--output', dest='output')\n+\n+    options, args = parser.parse_args()\n+    if len(args) > 1:\n+        print(\"More than one path given\")\n+        sys.exit(1)\n+\n+    path = args.pop() if args else '.'\n+    output = options.output or path\n+    suite = TestSuite(path, output)\n+    suite.load(options.force)\n+    suite.disable(options.excluded)\n+    if suite.write():\n+        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n+\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nnew file mode 100644\nindex 0000000000..3e8ae0a3a5\n--- /dev/null\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -0,0 +1,5 @@\n+clar.suite\n+.clarcache\n+clar_test\n+*.o\n+\ndiff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\nnew file mode 100644\nindex 0000000000..93c6b2ad32\n--- /dev/null\n+++ b/t/unit-tests/clar/test/Makefile\n@@ -0,0 +1,39 @@\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+#\n+# Set up the path to the clar sources and to the fixtures directory\n+#\n+# The fixture path needs to be an absolute path so it can be used\n+# even after we have chdir'ed into the test directory while testing.\n+#\n+CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n+TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n+CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n+CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n+\n+CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n+\n+.PHONY: clean\n+\n+# list the objects that go into our test\n+objects = main.o sample.o\n+\n+# build the test executable itself\n+clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n+\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n+\n+# test object files depend on clar macros\n+$(objects) : $(CLAR_PATH)clar.h\n+\n+# build the clar.suite file of test metadata\n+clar.suite:\n+\tpython \"$(CLAR_PATH)generate.py\" .\n+\n+# remove all generated files\n+clean:\n+\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\ndiff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\nnew file mode 100644\nindex 0000000000..0fcaa639aa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/clar_test.h\n@@ -0,0 +1,16 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST__\n+#define __CLAR_TEST__\n+\n+/* Import the standard clar helper functions */\n+#include \"clar.h\"\n+\n+/* Your custom shared includes / defines here */\n+extern int global_test_counter;\n+\n+#endif\ndiff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\nnew file mode 100644\nindex 0000000000..59e56ad255\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c\n@@ -0,0 +1,40 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Sample main() for clar tests.\n+ *\n+ * You should write your own main routine for clar tests that does specific\n+ * setup and teardown as necessary for your application.  The only required\n+ * line is the call to `clar_test(argc, argv)`, which will execute the test\n+ * suite.  If you want to check the return value of the test application,\n+ * your main() should return the same value returned by clar_test().\n+ */\n+\n+int global_test_counter = 0;\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\tint ret;\n+\n+\t/* Your custom initialization here */\n+\tglobal_test_counter = 0;\n+\n+\t/* Run the test suite */\n+\tret = clar_test(argc, argv);\n+\n+\t/* Your custom cleanup here */\n+\tcl_assert_equal_i(8, global_test_counter);\n+\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\nnew file mode 100644\nindex 0000000000..a4d91b72fa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c.sample\n@@ -0,0 +1,27 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Minimal main() for clar tests.\n+ *\n+ * Modify this with any application specific setup or teardown that you need.\n+ * The only required line is the call to `clar_test(argc, argv)`, which will\n+ * execute the test suite.  If you want to check the return value of the test\n+ * application, main() should return the same value returned by clar_test().\n+ */\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\t/* Run the test suite */\n+\treturn clar_test(argc, argv);\n+}\ndiff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\nnew file mode 100644\nindex 0000000000..220f4aa98a\n--- /dev/null\n+++ b/t/unit-tests/clar/test/resources/test/file\n@@ -0,0 +1 @@\n+File\ndiff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\nnew file mode 100644\nindex 0000000000..faa1209262\n--- /dev/null\n+++ b/t/unit-tests/clar/test/sample.c\n@@ -0,0 +1,84 @@\n+#include \"clar_test.h\"\n+#include <sys/stat.h>\n+\n+static int file_size(const char *filename)\n+{\n+\tstruct stat st;\n+\n+\tif (stat(filename, &st) == 0)\n+\t\treturn (int)st.st_size;\n+\treturn -1;\n+}\n+\n+void test_sample__initialize(void)\n+{\n+\tglobal_test_counter++;\n+}\n+\n+void test_sample__cleanup(void)\n+{\n+\tcl_fixture_cleanup(\"test\");\n+\n+\tcl_assert(file_size(\"test/file\") == -1);\n+}\n+\n+void test_sample__1(void)\n+{\n+\tcl_assert(1);\n+\tcl_must_pass(0);  /* 0 == success */\n+\tcl_must_fail(-1); /* <0 == failure */\n+\tcl_must_pass(-1); /* demonstrate a failing call */\n+}\n+\n+void test_sample__2(void)\n+{\n+\tcl_fixture_sandbox(\"test\");\n+\n+\tcl_assert(file_size(\"test/nonexistent\") == -1);\n+\tcl_assert(file_size(\"test/file\") > 0);\n+\tcl_assert(100 == 101);\n+}\n+\n+void test_sample__strings(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_s(\"expected\", actual);\n+\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n+\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n+}\n+\n+void test_sample__strings_with_length(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_strn(\"expected_\", actual, 8);\n+\tcl_assert_equal_strn(\"exactly\", actual, 2);\n+\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n+\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n+}\n+\n+void test_sample__int(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i(100, value);\n+\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n+}\n+\n+void test_sample__int_fmt(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n+}\n+\n+void test_sample__bool(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n+\tcl_assert_equal_b(0, value);\n+}\n+\n+void test_sample__ptr(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n+\tcl_assert_equal_p(&actual, actual);\n+}\n-- \n2.46.0.dirty\n\n"},{"id":"500396","messageId":"35682b7686e570a96a8432f6b8af1996ab8d748e.1723095269.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723095269.git.ps@pks.im","subject":"[RFC PATCH v3 3/7] t/clar: fix whitespace errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-08T05:38:22Z","receivedAt":"2024-08-08T05:38:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Fix whitespace errors in the clar that make git-apply(1) unhappy. This\nhas been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/97\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n t/unit-tests/clar/generate.py     | 1 -\n t/unit-tests/clar/test/.gitignore | 1 -\n 3 files changed, 2 insertions(+), 5 deletions(-)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 0ba1479620..7c177f3525 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -25,9 +25,9 @@ find_tmp_path(char *buffer, size_t length)\n \tstatic const size_t var_count = 5;\n \tstatic const char *env_vars[] = {\n \t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n- \t};\n+\t};\n \n- \tsize_t i;\n+\tsize_t i;\n \n \tfor (i = 0; i < var_count; ++i) {\n \t\tconst char *env = getenv(env_vars[i]);\n@@ -151,4 +151,3 @@ const char *clar_sandbox_path(void)\n {\n \treturn _clar_path;\n }\n-\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nindex 931b4d60d9..80996ac3e7 100755\n--- a/t/unit-tests/clar/generate.py\n+++ b/t/unit-tests/clar/generate.py\n@@ -264,4 +264,3 @@ def write(self):\n     suite.disable(options.excluded)\n     if suite.write():\n         print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n-\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nindex 3e8ae0a3a5..a477d0c40c 100644\n--- a/t/unit-tests/clar/test/.gitignore\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -2,4 +2,3 @@ clar.suite\n .clarcache\n clar_test\n *.o\n-\n-- \n2.46.0.dirty\n\n"},{"id":"500397","messageId":"7a76c21bcbe232c834bacc0f217371df4aef49c5.1723095269.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723095269.git.ps@pks.im","subject":"[RFC PATCH v3 4/7] t/clar: fix compatibility with NonStop","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-08T05:38:27Z","receivedAt":"2024-08-08T05:38:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The NonStop platform does not have `mkdtemp()` available, which we rely\non in `build_sandbox_path()`. Fix this issue by using `mktemp()` and\n`mkdir()` instead on this platform.\n\nThis has been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/96\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 7c177f3525..e25057b7c4 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -120,6 +120,12 @@ static int build_sandbox_path(void)\n \tif (_mktemp(_clar_path) == NULL)\n \t\treturn -1;\n \n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(__TANDEM)\n+\tif (mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n \tif (mkdir(_clar_path, 0700) != 0)\n \t\treturn -1;\n #elif defined(_WIN32)\n-- \n2.46.0.dirty\n\n"},{"id":"500398","messageId":"68b3c65951b590e75d61ee493aa252618ba72118.1723095269.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723095269.git.ps@pks.im","subject":"[RFC PATCH v3 5/7] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-08T05:38:31Z","receivedAt":"2024-08-08T05:38:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Wire up the clar unit testing framework by introducing a new\n\"unit-tests\" executable. In contrast to the existing framework, this\nwill result in a single executable for all test suites. The ability to\npick specific tests to execute is retained via functionality built into\nthe clar itself.\n\nNote that we need to be a bit careful about how we need to invalidate\nour Makefile rules. While we obviously have to regenerate the clar suite\nwhen our test suites change, we also have to invalidate it in case any\nof the test suites gets removed. We do so by using our typical pattern\nof creating a `GIT-TEST-SUITES` file that gets updated whenever the set\nof test suites changes, so that we can easily depend on that file.\n\nAnother specialty is that we generate a \"clar-decls.h\" file. The test\nfunctions are neither static, nor do they have external declarations.\nThis is because they are getting parsed via \"generate.py\", which then\ncreates the external generations that get populated into an array. These\ndeclarations are only seen by the main function though.\n\nThe consequence is that we will get a bunch of \"missing prototypes\"\nerrors from our compiler for each of these test functions. To fix those\nerrors, we extract the `extern` declarations from \"clar.suite\" and put\nthem into a standalone header that then gets included by each of our\nunit tests. This gets rid of compiler warnings for every function which\nhas been extracted by \"generate.py\". More importantly though, it does\n_not_ get rid of warnings in case a function really isn't being used by\nanything. Thus, it would cause a compiler error if a function name was\nmistyped and thus not picked up by \"generate.py\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Makefile                       | 34 ++++++++++++++++++++---\n t/Makefile                     |  1 +\n t/unit-tests/.gitignore        |  2 ++\n t/unit-tests/clar-generate.awk | 50 ++++++++++++++++++++++++++++++++++\n t/unit-tests/unit-test.c       | 18 ++++++++++++\n t/unit-tests/unit-test.h       |  3 ++\n 7 files changed, 105 insertions(+), 4 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\ndiff --git a/.gitignore b/.gitignore\nindex 8caf3700c2..6687bd6db4 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,6 +9,7 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n+/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex 674b0ac4e1..d343703215 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,6 +1334,11 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n+UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n+\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n@@ -2713,6 +2718,7 @@ OBJECTS += $(XDIFF_OBJS)\n OBJECTS += $(FUZZ_OBJS)\n OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n OBJECTS += $(UNIT_TEST_OBJS)\n+OBJECTS += $(UNIT_TESTS_OBJS)\n \n ifndef NO_CURL\n \tOBJECTS += http.o http-walker.o remote-curl.o\n@@ -3215,7 +3221,7 @@ endif\n \n test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))\n \n-all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)\n+all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n \n bin-wrappers/%: wrap-for-bin.sh\n \t$(call mkdir_p_parent_template)\n@@ -3646,7 +3652,7 @@ endif\n \n artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n \t\tGIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \\\n-\t\t$(UNIT_TEST_PROGS) $(MOFILES)\n+\t\t$(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) $(MOFILES)\n \t$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \\\n \t\tSHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'\n \ttest -n \"$(ARTIFACTS_DIRECTORY)\"\n@@ -3702,6 +3708,7 @@ cocciclean:\n \n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n+\t$(RM) GIT-TEST-SUITES $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/clar-decls.h\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3861,7 +3868,26 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n \n+GIT-TEST-SUITES: FORCE\n+\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n+\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n+\t\techo >&2 \"    * new test suites\"; \\\n+\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n+            fi\n+\n+$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n+\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n+\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n+\tdone >$@\n+$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n+\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n+$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR)\n+$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n+\t$(call mkdir_p_parent_template)\n+\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n+\n .PHONY: build-unit-tests unit-tests\n-build-unit-tests: $(UNIT_TEST_PROGS)\n-unit-tests: $(UNIT_TEST_PROGS) t/helper/test-tool$X\n+build-unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n+unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) t/helper/test-tool$X\n \t$(MAKE) -C t/ unit-tests\ndiff --git a/t/Makefile b/t/Makefile\nindex d2212de0b7..131ffd778f 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -48,6 +48,7 @@ CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.tes\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n+UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n UNIT_TESTS = $(sort $(UNIT_TEST_PROGRAMS))\n UNIT_TESTS_NO_DIR = $(notdir $(UNIT_TESTS))\n \ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex 5e56e040ec..d0632ec7f9 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1 +1,3 @@\n /bin\n+/clar.suite\n+/clar-decls.h\ndiff --git a/t/unit-tests/clar-generate.awk b/t/unit-tests/clar-generate.awk\nnew file mode 100644\nindex 0000000000..ab71ce6c9f\n--- /dev/null\n+++ b/t/unit-tests/clar-generate.awk\n@@ -0,0 +1,50 @@\n+function add_suite(suite, initialize, cleanup, count) {\n+       if (!suite) return\n+       suite_count++\n+       callback_count += count\n+       suites = suites \"    {\\n\"\n+       suites = suites \"        \\\"\" suite \"\\\",\\n\"\n+       suites = suites \"        \" initialize \",\\n\"\n+       suites = suites \"        \" cleanup \",\\n\"\n+       suites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n+       suites = suites \"    },\\n\"\n+}\n+\n+BEGIN {\n+       suites = \"static struct clar_suite _clar_suites[] = {\\n\"\n+}\n+\n+{\n+       print\n+       name = $3; sub(/\\(.*$/, \"\", name)\n+       suite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n+       short_name = name; sub(/^.*__/, \"\", short_name)\n+       cb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n+       if (suite != prev_suite) {\n+               add_suite(prev_suite, initialize, cleanup, count)\n+               if (callbacks) callbacks = callbacks \"};\\n\"\n+               callbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n+               initialize = \"{ NULL, NULL }\"\n+               cleanup = \"{ NULL, NULL }\"\n+               count = 0\n+               prev_suite = suite\n+       }\n+       if (short_name == \"initialize\") {\n+               initialize = cb\n+       } else if (short_name == \"cleanup\") {\n+               cleanup = cb\n+       } else {\n+               callbacks = callbacks \"    \" cb \",\\n\"\n+               count++\n+       }\n+}\n+\n+END {\n+       add_suite(suite, initialize, cleanup, count)\n+       suites = suites \"};\"\n+       if (callbacks) callbacks = callbacks \"};\"\n+       print callbacks\n+       print suites\n+       print \"static const size_t _clar_suite_count = \" suite_count \";\"\n+       print \"static const size_t _clar_callback_count = \" callback_count \";\"\n+}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nnew file mode 100644\nindex 0000000000..32a81299e9\n--- /dev/null\n+++ b/t/unit-tests/unit-test.c\n@@ -0,0 +1,18 @@\n+#include \"unit-test.h\"\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tconst char **argv_copy;\n+\tint ret;\n+\n+\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n+\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tCOPY_ARRAY(argv_copy, argv, argc);\n+\targv_copy[argc++] = \"-t\";\n+\targv_copy[argc] = NULL;\n+\n+\tret = clar_test(argc, (char **) argv_copy);\n+\n+\tfree(argv_copy);\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nnew file mode 100644\nindex 0000000000..66ec2387cc\n--- /dev/null\n+++ b/t/unit-tests/unit-test.h\n@@ -0,0 +1,3 @@\n+#include \"git-compat-util.h\"\n+#include \"clar/clar.h\"\n+#include \"clar-decls.h\"\n-- \n2.46.0.dirty\n\n"},{"id":"500399","messageId":"4a0888380e4f13bde852eec40926ff5c71ea3a37.1723095269.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723095269.git.ps@pks.im","subject":"[RFC PATCH v3 6/7] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-08T05:38:36Z","receivedAt":"2024-08-08T05:38:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the strvec tests to use the new clar unit testing framework.\nThis is a first test balloon that demonstrates how the testing infra for\nclar-based tests looks like.\n\nThe tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\nrunning that binary, it generates TAP output:\n\n    # ./t/unit-tests/bin/unit-tests\n    TAP version 13\n    # start of suite 1: strvec\n    ok 1 - strvec::init\n    ok 2 - strvec::dynamic_init\n    ok 3 - strvec::clear\n    ok 4 - strvec::push\n    ok 5 - strvec::pushft_pushf\n    ok 6 - strvec::pushl\n    ok 7 - strvec::pushv\n    ok 8 - strvec::replace_at_head\n    ok 9 - strvec::replace_at_tail\n    ok 10 - strvec::replace_in_between\n    ok 11 - strvec::replace_with_substring\n    ok 12 - strvec::remove_at_head\n    ok 13 - strvec::remove_at_tail\n    ok 14 - strvec::remove_in_between\n    ok 15 - strvec::pop_empty_array\n    ok 16 - strvec::pop_non_empty_array\n    ok 17 - strvec::split_empty_string\n    ok 18 - strvec::split_single_item\n    ok 19 - strvec::split_multiple_items\n    ok 20 - strvec::split_whitespace_only\n    ok 21 - strvec::split_multiple_consecutive_whitespaces\n    ok 22 - strvec::detach\n    1..22\n\nThe binary also supports some parameters that allow us to run only a\nsubset of unit tests or alter the output:\n\n    $ ./t/unit-tests/bin/unit-tests -h\n    Usage: ./t/unit-tests/bin/unit-tests [options]\n\n    Options:\n      -sname        Run only the suite with `name` (can go to individual test name)\n      -iname        Include the suite with `name`\n      -xname        Exclude the suite with `name`\n      -v            Increase verbosity (show suite names)\n      -q            Only report tests that had an error\n      -Q            Quit as soon as a test fails\n      -t            Display results in tap format\n      -l            Print suite names\n      -r[filename]  Write summary file (to the optional filename)\n\nFurthermore, running `make unit-tests` runs the binary along with all\nthe other unit tests we have.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                              |   2 +-\n t/unit-tests/{t-strvec.c => strvec.c} | 119 ++++++++++----------------\n t/unit-tests/unit-test.c              |   3 +-\n 3 files changed, 45 insertions(+), 79 deletions(-)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n\ndiff --git a/Makefile b/Makefile\nindex d343703215..ec90b8ac35 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,6 +1334,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n@@ -1351,7 +1352,6 @@ UNIT_TEST_PROGRAMS += t-reftable-merged\n UNIT_TEST_PROGRAMS += t-reftable-record\n UNIT_TEST_PROGRAMS += t-strbuf\n UNIT_TEST_PROGRAMS += t-strcmp-offset\n-UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-trailer\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/strvec.c\nsimilarity index 54%\nrename from t/unit-tests/t-strvec.c\nrename to t/unit-tests/strvec.c\nindex fa1a041469..d11ed0f28d 100644\n--- a/t/unit-tests/t-strvec.c\n+++ b/t/unit-tests/strvec.c\n@@ -1,52 +1,46 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n \n #define check_strvec(vec, ...) \\\n \tdo { \\\n \t\tconst char *expect[] = { __VA_ARGS__ }; \\\n-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n-\t\t\t\t\t\t (uintmax_t)i); \\\n-\t\t\t\t\tbreak; \\\n-\t\t\t\t} \\\n-\t\t\t} \\\n-\t\t} \\\n+\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n+\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n+\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n+\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n+\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n \t} while (0)\n \n-static void t_static_init(void)\n+void test_strvec__init(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_dynamic_init(void)\n+void test_strvec__dynamic_init(void)\n {\n \tstruct strvec vec;\n \tstrvec_init(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_clear(void)\n+void test_strvec__clear(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_push(&vec, \"foo\");\n \tstrvec_clear(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_push(void)\n+void test_strvec__push(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \n@@ -59,7 +53,7 @@ static void t_push(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushf(void)\n+void test_strvec__pushf(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushf(&vec, \"foo: %d\", 1);\n@@ -67,7 +61,7 @@ static void t_pushf(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushl(void)\n+void test_strvec__pushl(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -75,7 +69,7 @@ static void t_pushl(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushv(void)\n+void test_strvec__pushv(void)\n {\n \tconst char *strings[] = {\n \t\t\"foo\", \"bar\", \"baz\", NULL,\n@@ -88,7 +82,7 @@ static void t_pushv(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_head(void)\n+void test_strvec__replace_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -97,7 +91,7 @@ static void t_replace_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_tail(void)\n+void test_strvec__replace_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -106,7 +100,7 @@ static void t_replace_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_in_between(void)\n+void test_strvec__replace_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -115,7 +109,7 @@ static void t_replace_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_with_substring(void)\n+void test_strvec__replace_with_substring(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", NULL);\n@@ -124,7 +118,7 @@ static void t_replace_with_substring(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_head(void)\n+void test_strvec__remove_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -133,7 +127,7 @@ static void t_remove_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_tail(void)\n+void test_strvec__remove_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -142,7 +136,7 @@ static void t_remove_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_in_between(void)\n+void test_strvec__remove_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -151,7 +145,7 @@ static void t_remove_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_empty_array(void)\n+void test_strvec__pop_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pop(&vec);\n@@ -159,7 +153,7 @@ static void t_pop_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_non_empty_array(void)\n+void test_strvec__pop_non_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -168,7 +162,7 @@ static void t_pop_non_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_empty_string(void)\n+void test_strvec__split_empty_string(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"\");\n@@ -176,7 +170,7 @@ static void t_split_empty_string(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_single_item(void)\n+void test_strvec__split_single_item(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\");\n@@ -184,7 +178,7 @@ static void t_split_single_item(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_items(void)\n+void test_strvec__split_multiple_items(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo bar baz\");\n@@ -192,7 +186,7 @@ static void t_split_multiple_items(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_whitespace_only(void)\n+void test_strvec__split_whitespace_only(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \" \\t\\n\");\n@@ -200,7 +194,7 @@ static void t_split_whitespace_only(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_consecutive_whitespaces(void)\n+void test_strvec__split_multiple_consecutive_whitespaces(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\\n\\t bar\");\n@@ -208,7 +202,7 @@ static void t_split_multiple_consecutive_whitespaces(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_detach(void)\n+void test_strvec__detach(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tconst char **detached;\n@@ -216,40 +210,13 @@ static void t_detach(void)\n \tstrvec_push(&vec, \"foo\");\n \n \tdetached = strvec_detach(&vec);\n-\tcheck_str(detached[0], \"foo\");\n-\tcheck_pointer_eq(detached[1], NULL);\n+\tcl_assert_equal_s(detached[0], \"foo\");\n+\tcl_assert_equal_p(detached[1], NULL);\n \n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n \n \tfree((char *) detached[0]);\n \tfree(detached);\n }\n-\n-int cmd_main(int argc, const char **argv)\n-{\n-\tTEST(t_static_init(), \"static initialization\");\n-\tTEST(t_dynamic_init(), \"dynamic initialization\");\n-\tTEST(t_clear(), \"clear\");\n-\tTEST(t_push(), \"push\");\n-\tTEST(t_pushf(), \"pushf\");\n-\tTEST(t_pushl(), \"pushl\");\n-\tTEST(t_pushv(), \"pushv\");\n-\tTEST(t_replace_at_head(), \"replace at head\");\n-\tTEST(t_replace_in_between(), \"replace in between\");\n-\tTEST(t_replace_at_tail(), \"replace at tail\");\n-\tTEST(t_replace_with_substring(), \"replace with substring\");\n-\tTEST(t_remove_at_head(), \"remove at head\");\n-\tTEST(t_remove_in_between(), \"remove in between\");\n-\tTEST(t_remove_at_tail(), \"remove at tail\");\n-\tTEST(t_pop_empty_array(), \"pop with empty array\");\n-\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n-\tTEST(t_split_empty_string(), \"split empty string\");\n-\tTEST(t_split_single_item(), \"split single item\");\n-\tTEST(t_split_multiple_items(), \"split multiple items\");\n-\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n-\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n-\tTEST(t_detach(), \"detach\");\n-\treturn test_done();\n-}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nindex 32a81299e9..82b7635e6a 100644\n--- a/t/unit-tests/unit-test.c\n+++ b/t/unit-tests/unit-test.c\n@@ -6,10 +6,9 @@ int cmd_main(int argc, const char **argv)\n \tint ret;\n \n \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n-\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tALLOC_ARRAY(argv_copy, argc + 1);\n \tCOPY_ARRAY(argv_copy, argv, argc);\n \targv_copy[argc++] = \"-t\";\n-\targv_copy[argc] = NULL;\n \n \tret = clar_test(argc, (char **) argv_copy);\n \n-- \n2.46.0.dirty\n\n"},{"id":"500400","messageId":"f423b01c05586930a7f98e84f37b1b0d87d0a3f3.1723095269.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723095269.git.ps@pks.im","subject":"[RFC PATCH v3 7/7] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-08T05:38:41Z","receivedAt":"2024-08-08T05:38:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the ctype tests to use the new clar unit testing framework.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                            |  2 +-\n t/unit-tests/{t-ctype.c => ctype.c} | 71 +++++++++++++++++++++++------\n 2 files changed, 59 insertions(+), 14 deletions(-)\n rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n\ndiff --git a/Makefile b/Makefile\nindex ec90b8ac35..fa7c540bc0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,13 +1334,13 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += ctype\n UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n \n-UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n UNIT_TEST_PROGRAMS += t-mem-pool\ndiff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/ctype.c\nsimilarity index 71%\nrename from t/unit-tests/t-ctype.c\nrename to t/unit-tests/ctype.c\nindex d6ac1fe678..311df3a539 100644\n--- a/t/unit-tests/t-ctype.c\n+++ b/t/unit-tests/ctype.c\n@@ -1,18 +1,12 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n \n #define TEST_CHAR_CLASS(class, string) do { \\\n \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n-\tint skip = test__run_begin(); \\\n-\tif (!skip) { \\\n-\t\tfor (int i = 0; i < 256; i++) { \\\n-\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n-\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n-\t\t} \\\n-\t\tcheck(!class(EOF)); \\\n-\t} \\\n-\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n+\tfor (int i = 0; i < 256; i++) \\\n+\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n+\tcl_assert(!class(EOF)); \\\n } while (0)\n \n #define DIGIT \"0123456789\"\n@@ -33,21 +27,72 @@\n \t\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\" \\\n \t\"\\x7f\"\n \n-int cmd_main(int argc, const char **argv) {\n+void test_ctype__isspace(void)\n+{\n \tTEST_CHAR_CLASS(isspace, \" \\n\\r\\t\");\n+}\n+\n+void test_ctype__isdigit(void)\n+{\n \tTEST_CHAR_CLASS(isdigit, DIGIT);\n+}\n+\n+void test_ctype__isalpha(void)\n+{\n \tTEST_CHAR_CLASS(isalpha, LOWER UPPER);\n+}\n+\n+void test_ctype__isalnum(void)\n+{\n \tTEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);\n+}\n+\n+void test_ctype__is_glob_special(void)\n+{\n \tTEST_CHAR_CLASS(is_glob_special, \"*?[\\\\\");\n+}\n+\n+void test_ctype__is_regex_special(void)\n+{\n \tTEST_CHAR_CLASS(is_regex_special, \"$()*+.?[\\\\^{|\");\n+}\n+\n+void test_ctype__is_pathspec_magic(void)\n+{\n \tTEST_CHAR_CLASS(is_pathspec_magic, \"!\\\"#%&',-/:;<=>@_`~\");\n+}\n+\n+void test_ctype__isascii(void)\n+{\n \tTEST_CHAR_CLASS(isascii, ASCII);\n+}\n+\n+void test_ctype__islower(void)\n+{\n \tTEST_CHAR_CLASS(islower, LOWER);\n+}\n+\n+void test_ctype__isupper(void)\n+{\n \tTEST_CHAR_CLASS(isupper, UPPER);\n+}\n+\n+void test_ctype__iscntrl(void)\n+{\n \tTEST_CHAR_CLASS(iscntrl, CNTRL);\n+}\n+\n+void test_ctype__ispunct(void)\n+{\n \tTEST_CHAR_CLASS(ispunct, PUNCT);\n+}\n+\n+void test_ctype__isxdigit(void)\n+{\n \tTEST_CHAR_CLASS(isxdigit, DIGIT \"abcdefABCDEF\");\n-\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n+}\n \n-\treturn test_done();\n+void test_ctype__isprint(void)\n+{\n+\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n }\n-- \n2.46.0.dirty\n\n"},{"id":"500653","messageId":"k6uar46lu7ffbnboguca65afwegpcpfrivzfm7gspcorfefxnw@csqkd7wsoggk","threadId":"61874","inReplyTo":"cover.1723095269.git.ps@pks.im","subject":"Re: [RFC PATCH v3 0/7] Introduce clar testing framework","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2024-08-12T18:10:46Z","receivedAt":"2024-08-12T18:10:53Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2024.08.08 07:38, Patrick Steinhardt wrote:\n> Hi,\n> \n> this is the third version of my RFC patch series that introduces the\n> clar testing framework into our unit tests. The intent is to not have to\n> hand-craft all features of a proper unit testing framework, while still\n> not painting us into a corner. As such, the clar itself is small and\n> extensible while still bringing some nice features to the table.\n> \n> Changes compared to v2:\n> \n>   - Fix a copy/paste error for the clar license. It's ISC, not LGPL.\n> \n>   - Include \"clar.h\" via \"clar/clar.h\" such that we do not have to add\n>     \"clar/\" as in preprocessor include directive.\n> \n>   - Adapt strvec unit test to use `cl_assert_equal_i()` instead of\n>     `cl_assert()`.\n> \n> Thanks!\n> \n> Patrick\n> \n> Patrick Steinhardt (7):\n>   t: do not pass GIT_TEST_OPTS to unit tests with prove\n>   t: import the clar unit testing framework\n>   t/clar: fix whitespace errors\n>   t/clar: fix compatibility with NonStop\n>   Makefile: wire up the clar unit testing framework\n>   t/unit-tests: convert strvec tests to use clar\n>   t/unit-tests: convert ctype tests to use clar\n\nI'm generally in favor of this change, but I'm still unsure what our\nplan is for importing this from upstream clar. Are we going to vendor\nour own copy here and (hopefully) someone will pay attention to upstream\nfixes and apply them to our copy? Or will we replace this with a\nsubmodule?\n"},{"id":"500655","messageId":"031401daece3$4f6d5eb0$ee481c10$@nexbridge.com","threadId":"61874","inReplyTo":"k6uar46lu7ffbnboguca65afwegpcpfrivzfm7gspcorfefxnw@csqkd7wsoggk","subject":"RE: [RFC PATCH v3 0/7] Introduce clar testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-12T18:13:13Z","receivedAt":"2024-08-12T18:13:30Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Monday, August 12, 2024 2:11 PM, Josh Steadmon wrote:\n>On 2024.08.08 07:38, Patrick Steinhardt wrote:\n>> Hi,\n>>\n>> this is the third version of my RFC patch series that introduces the\n>> clar testing framework into our unit tests. The intent is to not have\n>> to hand-craft all features of a proper unit testing framework, while\n>> still not painting us into a corner. As such, the clar itself is small\n>> and extensible while still bringing some nice features to the table.\n>>\n>> Changes compared to v2:\n>>\n>>   - Fix a copy/paste error for the clar license. It's ISC, not LGPL.\n>>\n>>   - Include \"clar.h\" via \"clar/clar.h\" such that we do not have to add\n>>     \"clar/\" as in preprocessor include directive.\n>>\n>>   - Adapt strvec unit test to use `cl_assert_equal_i()` instead of\n>>     `cl_assert()`.\n>>\n>> Thanks!\n>>\n>> Patrick\n>>\n>> Patrick Steinhardt (7):\n>>   t: do not pass GIT_TEST_OPTS to unit tests with prove\n>>   t: import the clar unit testing framework\n>>   t/clar: fix whitespace errors\n>>   t/clar: fix compatibility with NonStop\n>>   Makefile: wire up the clar unit testing framework\n>>   t/unit-tests: convert strvec tests to use clar\n>>   t/unit-tests: convert ctype tests to use clar\n>\n>I'm generally in favor of this change, but I'm still unsure what our plan\nis for\n>importing this from upstream clar. Are we going to vendor our own copy here\nand\n>(hopefully) someone will pay attention to upstream fixes and apply them to\nour\n>copy? Or will we replace this with a submodule?\n\nI think we have our own copy of clar, customized to integrate with the\nexisting make\ninfrastructure.\n\n"},{"id":"500659","messageId":"xmqqy151zcsc.fsf@gitster.g","threadId":"61874","inReplyTo":"k6uar46lu7ffbnboguca65afwegpcpfrivzfm7gspcorfefxnw@csqkd7wsoggk","subject":"Re: [RFC PATCH v3 0/7] Introduce clar testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-12T20:50:11Z","receivedAt":"2024-08-12T20:50:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n> I'm generally in favor of this change, but I'm still unsure what our\n> plan is for importing this from upstream clar. Are we going to vendor\n> our own copy here and (hopefully) someone will pay attention to upstream\n> fixes and apply them to our copy? Or will we replace this with a\n> submodule?\n\nAs long as we do not have to make any changes to the \"vendored\" code\nourselves, that would not matter.  We will not randomly update the\ngitlink that specifies \"we want to use _this_ version and not other\nversion of upstream clar\" without good reasons if you are using it\nas a submodule, and we would need to justify why we are updating the\nhierarchy if we import the hierarchy as vendored source.  So the hassle\nof \"updating from upstream\" is pretty much the same.\n\nFor something as small as \"clar\", I think it is fine to start with\nthe currently proposed layout and see what happens.  If we can keep\ngoing without touching the imported part of the sources at all, and\nthe system proves to be useful and stable, that is a good time to\nsuggest moving it out and binding the selected version of the\nupstream as a submodule.\n\nThanks.\n\n"},{"id":"500660","messageId":"033401daecfa$773fa5d0$65bef170$@nexbridge.com","threadId":"61874","inReplyTo":"xmqqy151zcsc.fsf@gitster.g","subject":"RE: [RFC PATCH v3 0/7] Introduce clar testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-12T20:58:58Z","receivedAt":"2024-08-12T20:59:14Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Monday, August 12, 2024 4:50 PM, Junio C Hamano wrote:\n>Josh Steadmon <steadmon@google.com> writes:\n>\n>> I'm generally in favor of this change, but I'm still unsure what our\n>> plan is for importing this from upstream clar. Are we going to vendor\n>> our own copy here and (hopefully) someone will pay attention to\n>> upstream fixes and apply them to our copy? Or will we replace this\n>> with a submodule?\n>\n>As long as we do not have to make any changes to the \"vendored\" code ourselves,\n>that would not matter.  We will not randomly update the gitlink that specifies \"we\n>want to use _this_ version and not other version of upstream clar\" without good\n>reasons if you are using it as a submodule, and we would need to justify why we\n>are updating the hierarchy if we import the hierarchy as vendored source.  So the\n>hassle of \"updating from upstream\" is pretty much the same.\n>\n>For something as small as \"clar\", I think it is fine to start with the currently proposed\n>layout and see what happens.  If we can keep going without touching the imported\n>part of the sources at all, and the system proves to be useful and stable, that is a\n>good time to suggest moving it out and binding the selected version of the\n>upstream as a submodule.\n\nI think we already have a copy customized for git's use. The main clar repo on its own\nhas portability issues. I have contributed a few fixes, but they need work.\n\n"},{"id":"500671","messageId":"xmqqplqd9yp8.fsf@gitster.g","threadId":"61874","inReplyTo":"033401daecfa$773fa5d0$65bef170$@nexbridge.com","subject":"Re: [RFC PATCH v3 0/7] Introduce clar testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-12T22:13:39Z","receivedAt":"2024-08-12T22:13:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"<rsbecker@nexbridge.com> writes:\n\n>>For something as small as \"clar\", I think it is fine to start with the currently proposed\n>>layout and see what happens.  If we can keep going without touching the imported\n>>part of the sources at all, and the system proves to be useful and stable, that is a\n>>good time to suggest moving it out and binding the selected version of the\n>>upstream as a submodule.\n>\n> I think we already have a copy customized for git's use. The main clar repo on its own\n> has portability issues. I have contributed a few fixes, but they need work.\n\nYup, but as long as the changes we make are all upstreamable, the\nstory does not change all that much.  Changes like \"#ifdef TANDEM\"\nwould be totally uncontroversial thing for them to accept and we\nshould be able to upstream them fairly easily, and once we thin our\nlocal customization down to zero, we'd reach the state I outlined.\n\nStarting out with a local copy helps us making these portability and\nother changes without much friction, regardless of how responsive\nthe upstream is, and the request upstream would see is \"here are the\nchanges to make it available on more platforms and/or making it\ngenerally more useful. all of these changes have been used and\nbattle-tested in the context of the Git project for N months, please\napply.\"\n"},{"id":"500700","messageId":"ZrsJ6sYURdsxAZiQ@tanuki","threadId":"61874","inReplyTo":"xmqqplqd9yp8.fsf@gitster.g","subject":"Re: [RFC PATCH v3 0/7] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-13T07:23:22Z","receivedAt":"2024-08-13T07:23:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Aug 12, 2024 at 03:13:39PM -0700, Junio C Hamano wrote:\n> <rsbecker@nexbridge.com> writes:\n> \n> >>For something as small as \"clar\", I think it is fine to start with the currently proposed\n> >>layout and see what happens.  If we can keep going without touching the imported\n> >>part of the sources at all, and the system proves to be useful and stable, that is a\n> >>good time to suggest moving it out and binding the selected version of the\n> >>upstream as a submodule.\n> >\n> > I think we already have a copy customized for git's use. The main clar repo on its own\n> > has portability issues. I have contributed a few fixes, but they need work.\n> \n> Yup, but as long as the changes we make are all upstreamable, the\n> story does not change all that much.  Changes like \"#ifdef TANDEM\"\n> would be totally uncontroversial thing for them to accept and we\n> should be able to upstream them fairly easily, and once we thin our\n> local customization down to zero, we'd reach the state I outlined.\n> \n> Starting out with a local copy helps us making these portability and\n> other changes without much friction, regardless of how responsive\n> the upstream is, and the request upstream would see is \"here are the\n> changes to make it available on more platforms and/or making it\n> generally more useful. all of these changes have been used and\n> battle-tested in the context of the Git project for N months, please\n> apply.\"\n\nYeah, agreed. My intention certainly is to upstream all required\nchanges. Right now, it's only two minor fixes that we need, one for\nNonStop and one for whitespace fixes.\n\nWhile I'm still one of the maintainers of the clar, I first want to\nconnect with Ed before merging any of these fixes. I would otherwise\nconsider it a bit overreaching to just go in and merge things after\nhaving been absent from that project for such a long time. But when\nthings are sorted out I think the upstreaming process should be easy\nenough.\n\nMeanwhile, I think keeping this in the tree is fine. We can then\noptionally convert the code to a submodule once the necessary changes\nare upstream.\n\nPatrick\n"},{"id":"500795","messageId":"xmqqv8048mx2.fsf@gitster.g","threadId":"61874","inReplyTo":"35682b7686e570a96a8432f6b8af1996ab8d748e.1723095269.git.ps@pks.im","subject":"Re: [RFC PATCH v3 3/7] t/clar: fix whitespace errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-13T15:25:45Z","receivedAt":"2024-08-13T15:25:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Fix whitespace errors in the clar that make git-apply(1) unhappy. This\n> has been cherry-picked from the upstream pull request at [1].\n>\n> [1]: https://github.com/clar-test/clar/pull/97\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n>  t/unit-tests/clar/generate.py     | 1 -\n>  t/unit-tests/clar/test/.gitignore | 1 -\n>  3 files changed, 2 insertions(+), 5 deletions(-)\n\nA funny thing about this step is that the copy I carried for the\npast few days was an empty patch, because out of habit I use the\n\"--whitespace=fix\" option while running \"git am\".  As that fixes\nthe whitespace breakage this step addresses while applying the\nprevious step, this one becomes empty.\n\nI'll requeue these patches with --whitespace=warn instead.\n\nThanks.\n"},{"id":"500796","messageId":"03d601daed95$d10a63c0$731f2b40$@nexbridge.com","threadId":"61874","inReplyTo":"xmqqv8048mx2.fsf@gitster.g","subject":"RE: [RFC PATCH v3 3/7] t/clar: fix whitespace errors","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-13T15:31:01Z","receivedAt":"2024-08-13T15:31:21Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Tuesday, August 13, 2024 11:26 AM, Junio C Hamano wrote:\n>Patrick Steinhardt <ps@pks.im> writes:\n>\n>> Fix whitespace errors in the clar that make git-apply(1) unhappy. This\n>> has been cherry-picked from the upstream pull request at [1].\n>>\n>> [1]: https://github.com/clar-test/clar/pull/97\n>>\n>> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>> ---\n>>  t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n>>  t/unit-tests/clar/generate.py     | 1 -\n>>  t/unit-tests/clar/test/.gitignore | 1 -\n>>  3 files changed, 2 insertions(+), 5 deletions(-)\n>\n>A funny thing about this step is that the copy I carried for the past few days was an\n>empty patch, because out of habit I use the \"--whitespace=fix\" option while running\n>\"git am\".  As that fixes the whitespace breakage this step addresses while applying\n>the previous step, this one becomes empty.\n>\n>I'll requeue these patches with --whitespace=warn instead.\n\nPlease forgive the third ask, but I really want to test the build early. Suggestions?\n\n"},{"id":"500813","messageId":"xmqqzfpg461i.fsf@gitster.g","threadId":"61874","inReplyTo":"03d601daed95$d10a63c0$731f2b40$@nexbridge.com","subject":"Re: [RFC PATCH v3 3/7] t/clar: fix whitespace errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-13T18:43:53Z","receivedAt":"2024-08-13T18:43:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"<rsbecker@nexbridge.com> writes:\n\n> On Tuesday, August 13, 2024 11:26 AM, Junio C Hamano wrote:\n>>Patrick Steinhardt <ps@pks.im> writes:\n>>\n>>> Fix whitespace errors in the clar that make git-apply(1) unhappy. This\n>>> has been cherry-picked from the upstream pull request at [1].\n>>>\n>>> [1]: https://github.com/clar-test/clar/pull/97\n>>>\n>>> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>>> ---\n>>>  t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n>>>  t/unit-tests/clar/generate.py     | 1 -\n>>>  t/unit-tests/clar/test/.gitignore | 1 -\n>>>  3 files changed, 2 insertions(+), 5 deletions(-)\n>>\n>>A funny thing about this step is that the copy I carried for the past few days was an\n>>empty patch, because out of habit I use the \"--whitespace=fix\" option while running\n>>\"git am\".  As that fixes the whitespace breakage this step addresses while applying\n>>the previous step, this one becomes empty.\n>>\n>>I'll requeue these patches with --whitespace=warn instead.\n>\n> Please forgive the third ask, but I really want to test the build early. Suggestions?\n\nSorry, but I do not follow.  If you have \"git am\" and \"git clone\"\nworking, and if you have enough C toolchain to build the rest of\nGit, wouldn't it be sufficient to also build this addition?  I think\nthe 7 patches cleanly apply to any recent tip of 'master'.\n\n"},{"id":"500815","messageId":"03e701daedb5$083e1200$18ba3600$@nexbridge.com","threadId":"61874","inReplyTo":"xmqqzfpg461i.fsf@gitster.g","subject":"RE: [RFC PATCH v3 3/7] t/clar: fix whitespace errors","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-13T19:14:27Z","receivedAt":"2024-08-13T19:14:45Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Tuesday, August 13, 2024 2:44 PM, Junio C Hamano wrote:\n><rsbecker@nexbridge.com> writes:\n>\n>> On Tuesday, August 13, 2024 11:26 AM, Junio C Hamano wrote:\n>>>Patrick Steinhardt <ps@pks.im> writes:\n>>>\n>>>> Fix whitespace errors in the clar that make git-apply(1) unhappy.\n>>>> This has been cherry-picked from the upstream pull request at [1].\n>>>>\n>>>> [1]: https://github.com/clar-test/clar/pull/97\n>>>>\n>>>> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>>>> ---\n>>>>  t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n>>>>  t/unit-tests/clar/generate.py     | 1 -\n>>>>  t/unit-tests/clar/test/.gitignore | 1 -\n>>>>  3 files changed, 2 insertions(+), 5 deletions(-)\n>>>\n>>>A funny thing about this step is that the copy I carried for the past\n>>>few days was an empty patch, because out of habit I use the\n>>>\"--whitespace=fix\" option while running \"git am\".  As that fixes the\n>>>whitespace breakage this step addresses while applying the previous step, this\n>one becomes empty.\n>>>\n>>>I'll requeue these patches with --whitespace=warn instead.\n>>\n>> Please forgive the third ask, but I really want to test the build early. Suggestions?\n>\n>Sorry, but I do not follow.  If you have \"git am\" and \"git clone\"\n>working, and if you have enough C toolchain to build the rest of Git, wouldn't it be\n>sufficient to also build this addition?  I think the 7 patches cleanly apply to any\n>recent tip of 'master'.\n\nI am confused. My bad. The unit test works fine with the custom clar off seen and next.\n\n"},{"id":"500819","messageId":"xmqqcymc40kf.fsf@gitster.g","threadId":"61874","inReplyTo":"xmqqv8048mx2.fsf@gitster.g","subject":"Re: [RFC PATCH v3 3/7] t/clar: fix whitespace errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-13T20:42:08Z","receivedAt":"2024-08-13T20:42:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> Fix whitespace errors in the clar that make git-apply(1) unhappy. This\n>> has been cherry-picked from the upstream pull request at [1].\n>>\n>> [1]: https://github.com/clar-test/clar/pull/97\n>>\n>> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>> ---\n>>  t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n>>  t/unit-tests/clar/generate.py     | 1 -\n>>  t/unit-tests/clar/test/.gitignore | 1 -\n>>  3 files changed, 2 insertions(+), 5 deletions(-)\n>\n> A funny thing about this step is that the copy I carried for the\n> past few days was an empty patch, because out of habit I use the\n> \"--whitespace=fix\" option while running \"git am\".  As that fixes\n> the whitespace breakage this step addresses while applying the\n> previous step, this one becomes empty.\n>\n> I'll requeue these patches with --whitespace=warn instead.\n\nOops, it turns out that this is insufficient.  I'll add the\nfollowing as a separate patch immediately after this step.\n\nThanks.\n\n t/unit-tests/clar/clar/fs.h | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git c/t/unit-tests/clar/clar/fs.h w/t/unit-tests/clar/clar/fs.h\nindex a6eda5e5dc..3e39890bd3 100644\n--- c/t/unit-tests/clar/clar/fs.h\n+++ w/t/unit-tests/clar/clar/fs.h\n@@ -146,7 +146,7 @@ fs_rm_wait(WCHAR *_wpath)\n \t\t\tERROR_PATH_NOT_FOUND == last_error)\n \t\t\treturn 0;\n \n-\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n \t}\n \twhile (retries++ <= RM_RETRY_COUNT);\n \n"},{"id":"500843","messageId":"ZrxHkQxgq9VqF3V1@tanuki","threadId":"61874","inReplyTo":"xmqqcymc40kf.fsf@gitster.g","subject":"Re: [RFC PATCH v3 3/7] t/clar: fix whitespace errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-14T05:58:41Z","receivedAt":"2024-08-14T05:58:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 13, 2024 at 01:42:08PM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > Patrick Steinhardt <ps@pks.im> writes:\n> >\n> >> Fix whitespace errors in the clar that make git-apply(1) unhappy. This\n> >> has been cherry-picked from the upstream pull request at [1].\n> >>\n> >> [1]: https://github.com/clar-test/clar/pull/97\n> >>\n> >> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> >> ---\n> >>  t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n> >>  t/unit-tests/clar/generate.py     | 1 -\n> >>  t/unit-tests/clar/test/.gitignore | 1 -\n> >>  3 files changed, 2 insertions(+), 5 deletions(-)\n> >\n> > A funny thing about this step is that the copy I carried for the\n> > past few days was an empty patch, because out of habit I use the\n> > \"--whitespace=fix\" option while running \"git am\".  As that fixes\n> > the whitespace breakage this step addresses while applying the\n> > previous step, this one becomes empty.\n> >\n> > I'll requeue these patches with --whitespace=warn instead.\n> \n> Oops, it turns out that this is insufficient.  I'll add the\n> following as a separate patch immediately after this step.\n> \n> Thanks.\n> \n>  t/unit-tests/clar/clar/fs.h | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n> \n> diff --git c/t/unit-tests/clar/clar/fs.h w/t/unit-tests/clar/clar/fs.h\n> index a6eda5e5dc..3e39890bd3 100644\n> --- c/t/unit-tests/clar/clar/fs.h\n> +++ w/t/unit-tests/clar/clar/fs.h\n> @@ -146,7 +146,7 @@ fs_rm_wait(WCHAR *_wpath)\n>  \t\t\tERROR_PATH_NOT_FOUND == last_error)\n>  \t\t\treturn 0;\n>  \n> -\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n> +\t\tSleep(RM_RETRY_DELAY * retries * retries);\n>  \t}\n>  \twhile (retries++ <= RM_RETRY_COUNT);\n\nAh, didn't notice that one, thanks. I've also updated the upstream pull\nrequest accordingly.\n\nPatrick\n"},{"id":"500974","messageId":"75e097dfa4a59715cd26f0e0e97213f20f416d00.1723712012.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723712012.git.ps@pks.im","subject":"[PATCH v4 4/7] t/clar: fix compatibility with NonStop","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-15T09:47:45Z","receivedAt":"2024-08-15T11:21:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The NonStop platform does not have `mkdtemp()` available, which we rely\non in `build_sandbox_path()`. Fix this issue by using `mktemp()` and\n`mkdir()` instead on this platform.\n\nThis has been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/96\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 7c177f3525..e25057b7c4 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -120,6 +120,12 @@ static int build_sandbox_path(void)\n \tif (_mktemp(_clar_path) == NULL)\n \t\treturn -1;\n \n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(__TANDEM)\n+\tif (mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n \tif (mkdir(_clar_path, 0700) != 0)\n \t\treturn -1;\n #elif defined(_WIN32)\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"500975","messageId":"0a7fe8775a5e879c724c2b2b338fe0e26fc00234.1723712012.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723712012.git.ps@pks.im","subject":"[PATCH v4 7/7] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-15T09:47:56Z","receivedAt":"2024-08-15T11:21:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the ctype tests to use the new clar unit testing framework.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                            |  2 +-\n t/unit-tests/{t-ctype.c => ctype.c} | 71 +++++++++++++++++++++++------\n 2 files changed, 59 insertions(+), 14 deletions(-)\n rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n\ndiff --git a/Makefile b/Makefile\nindex ebc12b2bf1..d28faabbf4 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,13 +1334,13 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += ctype\n UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n \n-UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n UNIT_TEST_PROGRAMS += t-mem-pool\ndiff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/ctype.c\nsimilarity index 71%\nrename from t/unit-tests/t-ctype.c\nrename to t/unit-tests/ctype.c\nindex d6ac1fe678..311df3a539 100644\n--- a/t/unit-tests/t-ctype.c\n+++ b/t/unit-tests/ctype.c\n@@ -1,18 +1,12 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n \n #define TEST_CHAR_CLASS(class, string) do { \\\n \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n-\tint skip = test__run_begin(); \\\n-\tif (!skip) { \\\n-\t\tfor (int i = 0; i < 256; i++) { \\\n-\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n-\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n-\t\t} \\\n-\t\tcheck(!class(EOF)); \\\n-\t} \\\n-\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n+\tfor (int i = 0; i < 256; i++) \\\n+\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n+\tcl_assert(!class(EOF)); \\\n } while (0)\n \n #define DIGIT \"0123456789\"\n@@ -33,21 +27,72 @@\n \t\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\" \\\n \t\"\\x7f\"\n \n-int cmd_main(int argc, const char **argv) {\n+void test_ctype__isspace(void)\n+{\n \tTEST_CHAR_CLASS(isspace, \" \\n\\r\\t\");\n+}\n+\n+void test_ctype__isdigit(void)\n+{\n \tTEST_CHAR_CLASS(isdigit, DIGIT);\n+}\n+\n+void test_ctype__isalpha(void)\n+{\n \tTEST_CHAR_CLASS(isalpha, LOWER UPPER);\n+}\n+\n+void test_ctype__isalnum(void)\n+{\n \tTEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);\n+}\n+\n+void test_ctype__is_glob_special(void)\n+{\n \tTEST_CHAR_CLASS(is_glob_special, \"*?[\\\\\");\n+}\n+\n+void test_ctype__is_regex_special(void)\n+{\n \tTEST_CHAR_CLASS(is_regex_special, \"$()*+.?[\\\\^{|\");\n+}\n+\n+void test_ctype__is_pathspec_magic(void)\n+{\n \tTEST_CHAR_CLASS(is_pathspec_magic, \"!\\\"#%&',-/:;<=>@_`~\");\n+}\n+\n+void test_ctype__isascii(void)\n+{\n \tTEST_CHAR_CLASS(isascii, ASCII);\n+}\n+\n+void test_ctype__islower(void)\n+{\n \tTEST_CHAR_CLASS(islower, LOWER);\n+}\n+\n+void test_ctype__isupper(void)\n+{\n \tTEST_CHAR_CLASS(isupper, UPPER);\n+}\n+\n+void test_ctype__iscntrl(void)\n+{\n \tTEST_CHAR_CLASS(iscntrl, CNTRL);\n+}\n+\n+void test_ctype__ispunct(void)\n+{\n \tTEST_CHAR_CLASS(ispunct, PUNCT);\n+}\n+\n+void test_ctype__isxdigit(void)\n+{\n \tTEST_CHAR_CLASS(isxdigit, DIGIT \"abcdefABCDEF\");\n-\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n+}\n \n-\treturn test_done();\n+void test_ctype__isprint(void)\n+{\n+\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n }\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"500976","messageId":"cover.1723712012.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[PATCH v4 0/7] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-15T09:47:31Z","receivedAt":"2024-08-15T11:21:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the fourth version of my patch series that introduces the clar\ntesting framework for our unit tests.\n\nChanges compared to v3:\n\n  - Seeing that the overall feedback seems to be rather positive, I've\n    now dropped the \"RFC\" prefix.\n\n  - Fix another whitespace-damaged line in the clar unit test. I've\n    adapted the upstream pull request accordingly.\n\n  - Fix `make hdr-check`, which failed due to the missing dependency on\n    \"clar-decls.h\".\n\nThanks!\n\nPatrick\n\nPatrick Steinhardt (7):\n  t: do not pass GIT_TEST_OPTS to unit tests with prove\n  t: import the clar unit testing framework\n  t/clar: fix whitespace errors\n  t/clar: fix compatibility with NonStop\n  Makefile: wire up the clar unit testing framework\n  t/unit-tests: convert strvec tests to use clar\n  t/unit-tests: convert ctype tests to use clar\n\n .gitignore                                 |   1 +\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |  43 +-\n t/Makefile                                 |   4 +-\n t/run-test.sh                              |   2 +-\n t/unit-tests/.gitignore                    |   2 +\n t/unit-tests/clar-generate.awk             |  50 ++\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n t/unit-tests/{t-strvec.c => strvec.c}      | 119 ++-\n t/unit-tests/unit-test.c                   |  17 +\n t/unit-tests/unit-test.h                   |   3 +\n 29 files changed, 3160 insertions(+), 98 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\nRange-diff against v3:\n1:  78a9cc1162 = 1:  086dd728a7 t: do not pass GIT_TEST_OPTS to unit tests with prove\n2:  b6c066ee4e = 2:  5c22e0b3b9 t: import the clar unit testing framework\n3:  35682b7686 ! 3:  e0f99874cc t/clar: fix whitespace errors\n    @@ Commit message\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    + ## t/unit-tests/clar/clar/fs.h ##\n    +@@ t/unit-tests/clar/clar/fs.h: fs_rm_wait(WCHAR *_wpath)\n    + \t\t\tERROR_PATH_NOT_FOUND == last_error)\n    + \t\t\treturn 0;\n    + \n    +-\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n    ++\t\tSleep(RM_RETRY_DELAY * retries * retries);\n    + \t}\n    + \twhile (retries++ <= RM_RETRY_COUNT);\n    + \n    +\n      ## t/unit-tests/clar/clar/sandbox.h ##\n     @@ t/unit-tests/clar/clar/sandbox.h: find_tmp_path(char *buffer, size_t length)\n      \tstatic const size_t var_count = 5;\n4:  7a76c21bcb = 4:  75e097dfa4 t/clar: fix compatibility with NonStop\n5:  68b3c65951 ! 5:  5b8a64ae79 Makefile: wire up the clar unit testing framework\n    @@ Makefile: endif\n      \n      bin-wrappers/%: wrap-for-bin.sh\n      \t$(call mkdir_p_parent_template)\n    +@@ Makefile: CHK_HDRS = $(filter-out $(EXCEPT_HDRS),$(LIB_H))\n    + HCO = $(patsubst %.h,%.hco,$(CHK_HDRS))\n    + HCC = $(HCO:hco=hcc)\n    + \n    ++$(UNIT_TEST_DIR)/unit-test.hcc: $(UNIT_TEST_DIR)/unit-test.h $(UNIT_TEST_DIR)/clar-decls.h\n    + %.hcc: %.h\n    + \t@echo '#include \"git-compat-util.h\"' >$@\n    + \t@echo '#include \"$<\"' >>$@\n     @@ Makefile: endif\n      \n      artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n6:  4a0888380e = 6:  bc4e23d666 t/unit-tests: convert strvec tests to use clar\n7:  f423b01c05 = 7:  0a7fe8775a t/unit-tests: convert ctype tests to use clar\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"500977","messageId":"bc4e23d666d23cc0cf74d9ede4a2e446185eec1e.1723712012.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723712012.git.ps@pks.im","subject":"[PATCH v4 6/7] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-15T09:47:53Z","receivedAt":"2024-08-15T11:21:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the strvec tests to use the new clar unit testing framework.\nThis is a first test balloon that demonstrates how the testing infra for\nclar-based tests looks like.\n\nThe tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\nrunning that binary, it generates TAP output:\n\n    # ./t/unit-tests/bin/unit-tests\n    TAP version 13\n    # start of suite 1: strvec\n    ok 1 - strvec::init\n    ok 2 - strvec::dynamic_init\n    ok 3 - strvec::clear\n    ok 4 - strvec::push\n    ok 5 - strvec::pushft_pushf\n    ok 6 - strvec::pushl\n    ok 7 - strvec::pushv\n    ok 8 - strvec::replace_at_head\n    ok 9 - strvec::replace_at_tail\n    ok 10 - strvec::replace_in_between\n    ok 11 - strvec::replace_with_substring\n    ok 12 - strvec::remove_at_head\n    ok 13 - strvec::remove_at_tail\n    ok 14 - strvec::remove_in_between\n    ok 15 - strvec::pop_empty_array\n    ok 16 - strvec::pop_non_empty_array\n    ok 17 - strvec::split_empty_string\n    ok 18 - strvec::split_single_item\n    ok 19 - strvec::split_multiple_items\n    ok 20 - strvec::split_whitespace_only\n    ok 21 - strvec::split_multiple_consecutive_whitespaces\n    ok 22 - strvec::detach\n    1..22\n\nThe binary also supports some parameters that allow us to run only a\nsubset of unit tests or alter the output:\n\n    $ ./t/unit-tests/bin/unit-tests -h\n    Usage: ./t/unit-tests/bin/unit-tests [options]\n\n    Options:\n      -sname        Run only the suite with `name` (can go to individual test name)\n      -iname        Include the suite with `name`\n      -xname        Exclude the suite with `name`\n      -v            Increase verbosity (show suite names)\n      -q            Only report tests that had an error\n      -Q            Quit as soon as a test fails\n      -t            Display results in tap format\n      -l            Print suite names\n      -r[filename]  Write summary file (to the optional filename)\n\nFurthermore, running `make unit-tests` runs the binary along with all\nthe other unit tests we have.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                              |   2 +-\n t/unit-tests/{t-strvec.c => strvec.c} | 119 ++++++++++----------------\n t/unit-tests/unit-test.c              |   3 +-\n 3 files changed, 45 insertions(+), 79 deletions(-)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n\ndiff --git a/Makefile b/Makefile\nindex 1311bbbbed..ebc12b2bf1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,6 +1334,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n@@ -1351,7 +1352,6 @@ UNIT_TEST_PROGRAMS += t-reftable-merged\n UNIT_TEST_PROGRAMS += t-reftable-record\n UNIT_TEST_PROGRAMS += t-strbuf\n UNIT_TEST_PROGRAMS += t-strcmp-offset\n-UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-trailer\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/strvec.c\nsimilarity index 54%\nrename from t/unit-tests/t-strvec.c\nrename to t/unit-tests/strvec.c\nindex fa1a041469..d11ed0f28d 100644\n--- a/t/unit-tests/t-strvec.c\n+++ b/t/unit-tests/strvec.c\n@@ -1,52 +1,46 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n \n #define check_strvec(vec, ...) \\\n \tdo { \\\n \t\tconst char *expect[] = { __VA_ARGS__ }; \\\n-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n-\t\t\t\t\t\t (uintmax_t)i); \\\n-\t\t\t\t\tbreak; \\\n-\t\t\t\t} \\\n-\t\t\t} \\\n-\t\t} \\\n+\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n+\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n+\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n+\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n+\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n \t} while (0)\n \n-static void t_static_init(void)\n+void test_strvec__init(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_dynamic_init(void)\n+void test_strvec__dynamic_init(void)\n {\n \tstruct strvec vec;\n \tstrvec_init(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_clear(void)\n+void test_strvec__clear(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_push(&vec, \"foo\");\n \tstrvec_clear(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_push(void)\n+void test_strvec__push(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \n@@ -59,7 +53,7 @@ static void t_push(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushf(void)\n+void test_strvec__pushf(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushf(&vec, \"foo: %d\", 1);\n@@ -67,7 +61,7 @@ static void t_pushf(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushl(void)\n+void test_strvec__pushl(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -75,7 +69,7 @@ static void t_pushl(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushv(void)\n+void test_strvec__pushv(void)\n {\n \tconst char *strings[] = {\n \t\t\"foo\", \"bar\", \"baz\", NULL,\n@@ -88,7 +82,7 @@ static void t_pushv(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_head(void)\n+void test_strvec__replace_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -97,7 +91,7 @@ static void t_replace_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_tail(void)\n+void test_strvec__replace_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -106,7 +100,7 @@ static void t_replace_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_in_between(void)\n+void test_strvec__replace_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -115,7 +109,7 @@ static void t_replace_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_with_substring(void)\n+void test_strvec__replace_with_substring(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", NULL);\n@@ -124,7 +118,7 @@ static void t_replace_with_substring(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_head(void)\n+void test_strvec__remove_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -133,7 +127,7 @@ static void t_remove_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_tail(void)\n+void test_strvec__remove_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -142,7 +136,7 @@ static void t_remove_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_in_between(void)\n+void test_strvec__remove_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -151,7 +145,7 @@ static void t_remove_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_empty_array(void)\n+void test_strvec__pop_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pop(&vec);\n@@ -159,7 +153,7 @@ static void t_pop_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_non_empty_array(void)\n+void test_strvec__pop_non_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -168,7 +162,7 @@ static void t_pop_non_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_empty_string(void)\n+void test_strvec__split_empty_string(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"\");\n@@ -176,7 +170,7 @@ static void t_split_empty_string(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_single_item(void)\n+void test_strvec__split_single_item(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\");\n@@ -184,7 +178,7 @@ static void t_split_single_item(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_items(void)\n+void test_strvec__split_multiple_items(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo bar baz\");\n@@ -192,7 +186,7 @@ static void t_split_multiple_items(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_whitespace_only(void)\n+void test_strvec__split_whitespace_only(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \" \\t\\n\");\n@@ -200,7 +194,7 @@ static void t_split_whitespace_only(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_consecutive_whitespaces(void)\n+void test_strvec__split_multiple_consecutive_whitespaces(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\\n\\t bar\");\n@@ -208,7 +202,7 @@ static void t_split_multiple_consecutive_whitespaces(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_detach(void)\n+void test_strvec__detach(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tconst char **detached;\n@@ -216,40 +210,13 @@ static void t_detach(void)\n \tstrvec_push(&vec, \"foo\");\n \n \tdetached = strvec_detach(&vec);\n-\tcheck_str(detached[0], \"foo\");\n-\tcheck_pointer_eq(detached[1], NULL);\n+\tcl_assert_equal_s(detached[0], \"foo\");\n+\tcl_assert_equal_p(detached[1], NULL);\n \n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n \n \tfree((char *) detached[0]);\n \tfree(detached);\n }\n-\n-int cmd_main(int argc, const char **argv)\n-{\n-\tTEST(t_static_init(), \"static initialization\");\n-\tTEST(t_dynamic_init(), \"dynamic initialization\");\n-\tTEST(t_clear(), \"clear\");\n-\tTEST(t_push(), \"push\");\n-\tTEST(t_pushf(), \"pushf\");\n-\tTEST(t_pushl(), \"pushl\");\n-\tTEST(t_pushv(), \"pushv\");\n-\tTEST(t_replace_at_head(), \"replace at head\");\n-\tTEST(t_replace_in_between(), \"replace in between\");\n-\tTEST(t_replace_at_tail(), \"replace at tail\");\n-\tTEST(t_replace_with_substring(), \"replace with substring\");\n-\tTEST(t_remove_at_head(), \"remove at head\");\n-\tTEST(t_remove_in_between(), \"remove in between\");\n-\tTEST(t_remove_at_tail(), \"remove at tail\");\n-\tTEST(t_pop_empty_array(), \"pop with empty array\");\n-\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n-\tTEST(t_split_empty_string(), \"split empty string\");\n-\tTEST(t_split_single_item(), \"split single item\");\n-\tTEST(t_split_multiple_items(), \"split multiple items\");\n-\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n-\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n-\tTEST(t_detach(), \"detach\");\n-\treturn test_done();\n-}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nindex 32a81299e9..82b7635e6a 100644\n--- a/t/unit-tests/unit-test.c\n+++ b/t/unit-tests/unit-test.c\n@@ -6,10 +6,9 @@ int cmd_main(int argc, const char **argv)\n \tint ret;\n \n \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n-\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tALLOC_ARRAY(argv_copy, argc + 1);\n \tCOPY_ARRAY(argv_copy, argv, argc);\n \targv_copy[argc++] = \"-t\";\n-\targv_copy[argc] = NULL;\n \n \tret = clar_test(argc, (char **) argv_copy);\n \n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"500978","messageId":"5c22e0b3b976c3ec249f073287ef0ee9ae835651.1723712012.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723712012.git.ps@pks.im","subject":"[PATCH v4 2/7] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-15T09:47:39Z","receivedAt":"2024-08-15T11:21:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Import the clar unit testing framework at commit faa8419 (Merge pull\nrequest #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |   4 +-\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 154 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 267 +++++++\n t/unit-tests/clar/test/.gitignore          |   5 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n 20 files changed, 2946 insertions(+), 1 deletion(-)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n\ndiff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt\nindex 206037ffb1..5a432b7b29 100644\n--- a/Documentation/technical/unit-tests.txt\n+++ b/Documentation/technical/unit-tests.txt\n@@ -203,6 +203,7 @@ GitHub / GitLab stars to estimate this.\n :criterion: https://github.com/Snaipe/Criterion[Criterion]\n :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n :check: https://libcheck.github.io/check/[Check]\n+:clar: https://github.com/clar-test/clar[Clar]\n \n [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n |=====\n@@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n+{clar},{isc},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n |=====\n \n === Additional framework candidates\ndiff --git a/Makefile b/Makefile\nindex 3863e60b66..674b0ac4e1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1331,6 +1331,8 @@ THIRD_PARTY_SOURCES += compat/poll/%\n THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n@@ -3260,7 +3262,7 @@ $(SP_OBJ): %.sp: %.c %.o\n .PHONY: sparse\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n ifndef OPENSSL_SHA1\n \tEXCEPT_HDRS += sha1/openssl.h\n endif\ndiff --git a/t/unit-tests/clar/.github/workflows/ci.yml b/t/unit-tests/clar/.github/workflows/ci.yml\nnew file mode 100644\nindex 0000000000..b1ac2de460\n--- /dev/null\n+++ b/t/unit-tests/clar/.github/workflows/ci.yml\n@@ -0,0 +1,23 @@\n+name: CI\n+\n+on:\n+  push:\n+    branches: [ main ]\n+  pull_request:\n+    branches: [ main ]\n+\n+jobs:\n+  build:\n+    strategy:\n+      matrix:\n+        os: [ ubuntu-latest, macos-latest ]\n+\n+    runs-on: ${{ matrix.os }}\n+\n+    steps:\n+    - name: Check out\n+      uses: actions/checkout@v2\n+    - name: Build\n+      run: |\n+        cd test\n+        make\ndiff --git a/t/unit-tests/clar/COPYING b/t/unit-tests/clar/COPYING\nnew file mode 100644\nindex 0000000000..8983817f0c\n--- /dev/null\n+++ b/t/unit-tests/clar/COPYING\n@@ -0,0 +1,15 @@\n+ISC License\n+\n+Copyright (c) 2011-2015 Vicent Marti\n+\n+Permission to use, copy, modify, and/or distribute this software for any\n+purpose with or without fee is hereby granted, provided that the above\n+copyright notice and this permission notice appear in all copies.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ndiff --git a/t/unit-tests/clar/README.md b/t/unit-tests/clar/README.md\nnew file mode 100644\nindex 0000000000..a8961c5f10\n--- /dev/null\n+++ b/t/unit-tests/clar/README.md\n@@ -0,0 +1,329 @@\n+Come out and Clar\n+=================\n+\n+In Catalan, \"clar\" means clear, easy to perceive.  Using clar will make it\n+easy to test and make clear the quality of your code.\n+\n+> _Historical note_\n+>\n+> Originally the clar project was named \"clay\" because the word \"test\" has its\n+> roots in the latin word *\"testum\"*, meaning \"earthen pot\", and *\"testa\"*,\n+> meaning \"piece of burned clay\"?\n+>\n+> This is because historically, testing implied melting metal in a pot to\n+> check its quality.  Clay is what tests are made of.\n+\n+## Quick Usage Overview\n+\n+Clar is a minimal C unit testing framework. It's been written to replace the\n+old framework in [libgit2][libgit2], but it's both very versatile and\n+straightforward to use.\n+\n+Can you count to funk?\n+\n+- **Zero: Initialize test directory**\n+\n+    ~~~~ sh\n+    $ mkdir tests\n+    $ cp -r $CLAR_ROOT/clar* tests\n+    $ cp $CLAR_ROOT/test/clar_test.h tests\n+    $ cp $CLAR_ROOT/test/main.c.sample tests/main.c\n+    ~~~~\n+\n+- **One: Write some tests**\n+\n+    File: tests/adding.c:\n+\n+    ~~~~ c\n+    /* adding.c for the \"Adding\" suite */\n+    #include \"clar.h\"\n+\n+    static int *answer;\n+\n+    void test_adding__initialize(void)\n+    {\n+        answer = malloc(sizeof(int));\n+        cl_assert_(answer != NULL, \"No memory left?\");\n+        *answer = 42;\n+    }\n+\n+    void test_adding__cleanup(void)\n+    {\n+        free(answer);\n+    }\n+\n+    void test_adding__make_sure_math_still_works(void)\n+    {\n+        cl_assert_(5 > 3, \"Five should probably be greater than three\");\n+        cl_assert_(-5 < 2, \"Negative numbers are small, I think\");\n+        cl_assert_(*answer == 42, \"The universe is doing OK. And the initializer too.\");\n+    }\n+    ~~~~~\n+\n+- **Two: Build the test executable**\n+\n+    ~~~~ sh\n+    $ cd tests\n+    $ $CLAR_PATH/generate.py .\n+    Written `clar.suite` (1 suites)\n+    $ gcc -I. clar.c main.c adding.c -o testit\n+    ~~~~\n+\n+- **Funk: Funk it.**\n+\n+    ~~~~ sh\n+    $ ./testit\n+    ~~~~\n+\n+## The Clar Test Suite\n+\n+Writing a test suite is pretty straightforward. Each test suite is a `*.c`\n+file with a descriptive name: this encourages modularity.\n+\n+Each test suite has optional initialize and cleanup methods. These methods\n+will be called before and after running **each** test in the suite, even if\n+such test fails. As a rule of thumb, if a test needs a different initializer\n+or cleanup method than another test in the same module, that means it\n+doesn't belong in that module. Keep that in mind when grouping tests\n+together.\n+\n+The `initialize` and `cleanup` methods have the following syntax, with\n+`suitename` being the current suite name, e.g. `adding` for the `adding.c`\n+suite.\n+\n+~~~~ c\n+void test_suitename__initialize(void)\n+{\n+    /* init */\n+}\n+\n+void test_suitename__cleanup(void)\n+{\n+    /* cleanup */\n+}\n+~~~~\n+\n+These methods are encouraged to use static, global variables to store the state\n+that will be used by all tests inside the suite.\n+\n+~~~~ c\n+static git_repository *_repository;\n+\n+void test_status__initialize(void)\n+{\n+    create_tmp_repo(STATUS_REPO);\n+    git_repository_open(_repository, STATUS_REPO);\n+}\n+\n+void test_status__cleanup(void)\n+{\n+    git_repository_close(_repository);\n+    git_path_rm(STATUS_REPO);\n+}\n+\n+void test_status__simple_test(void)\n+{\n+    /* do something with _repository */\n+}\n+~~~~\n+\n+Writing the actual tests is just as straightforward. Tests have the\n+`void test_suitename__test_name(void)` signature, and they should **not**\n+be static. Clar will automatically detect and list them.\n+\n+Tests are run as they appear on their original suites: they have no return\n+value. A test is considered \"passed\" if it doesn't raise any errors. Check\n+the \"Clar API\" section to see the various helper functions to check and\n+raise errors during test execution.\n+\n+__Caution:__ If you use assertions inside of `test_suitename__initialize`,\n+make sure that you do not rely on `__initialize` being completely run\n+inside your `test_suitename__cleanup` function. Otherwise you might\n+encounter ressource cleanup twice.\n+\n+## How does Clar work?\n+\n+To use Clar:\n+\n+1. copy the Clar boilerplate to your test directory\n+2. copy (and probably modify) the sample `main.c` (from\n+   `$CLAR_PATH/test/main.c.sample`)\n+3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and\n+   write out the test suite metadata.\n+4. compile your test files and the Clar boilerplate into a single test\n+   executable\n+5. run the executable to test!\n+\n+The Clar boilerplate gives you a set of useful test assertions and features\n+(like accessing or making sandbox copies of fixture data).  It consists of\n+the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory.\n+You should not need to edit these files.\n+\n+The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes\n+`clar_test(argc, argv)` to run the tests.  Usually, you will edit this file\n+to perform any framework specific initialization and teardown that you need.\n+\n+The Clar mixer (`generate.py`) recursively scans your test directory for\n+any `.c` files, parses them, and writes the `clar.suite` file with all of\n+the metadata about your tests.  When you build, the `clar.suite` file is\n+included into `clar.c`.\n+\n+The mixer can be run with **Python 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and PyPy 1.6**.\n+\n+Commandline usage of the mixer is as follows:\n+\n+    $ ./generate.py .\n+\n+Where `.` is the folder where all the test suites can be found. The mixer\n+will automatically locate all the relevant source files and build the\n+testing metadata. The metadata will be written to `clar.suite`, in the same\n+folder as all the test suites. This file is included by `clar.c` and so\n+must be accessible via `#include` when building the test executable.\n+\n+    $ gcc -I. clar.c main.c suite1.c test2.c -o run_tests\n+\n+**Note that the Clar mixer only needs to be ran when adding new tests to a\n+suite, in order to regenerate the metadata**. As a result, the `clar.suite`\n+file can be checked into version control if you wish to be able to build\n+your test suite without having to re-run the mixer.\n+\n+This is handy when e.g. generating tests in a local computer, and then\n+building and testing them on an embedded device or a platform where Python\n+is not available.\n+\n+### Fixtures\n+\n+Clar can create sandboxed fixtures for you to use in your test. You'll need to compile *clar.c* with an additional `CFLAG`, `-DCLAR_FIXTURE_PATH`. This should be an absolute path to your fixtures directory.\n+\n+Once that's done, you can use the fixture API as defined below.\n+\n+## The Clar API\n+\n+Clar makes the following methods available from all functions in a test\n+suite.\n+\n+-   `cl_must_pass(call)`, `cl_must_pass_(call, message)`: Verify that the given\n+    function call passes, in the POSIX sense (returns a value greater or equal\n+    to 0).\n+\n+-   `cl_must_fail(call)`, `cl_must_fail_(call, message)`: Verify that the given\n+    function call fails, in the POSIX sense (returns a value less than 0).\n+\n+-   `cl_assert(expr)`, `cl_assert_(expr, message)`: Verify that `expr` is true.\n+\n+-   `cl_check_pass(call)`, `cl_check_pass_(call, message)`: Verify that the\n+    given function call passes, in the POSIX sense (returns a value greater or\n+    equal to 0). If the function call doesn't succeed, a test failure will be\n+    logged but the test's execution will continue.\n+\n+-   `cl_check_fail(call)`, `cl_check_fail_(call, message)`: Verify that the\n+    given function call fails, in the POSIX sense (returns a value less than\n+    0). If the function call doesn't fail, a test failure will be logged but\n+    the test's execution will continue.\n+\n+-   `cl_check(expr)`: Verify that `expr` is true. If `expr` is not\n+    true, a test failure will be logged but the test's execution will continue.\n+\n+-   `cl_fail(message)`: Fail the current test with the given message.\n+\n+-   `cl_warning(message)`: Issue a warning. This warning will be\n+    logged as a test failure but the test's execution will continue.\n+\n+-   `cl_set_cleanup(void (*cleanup)(void *), void *opaque)`: Set the cleanup\n+    method for a single test. This method will be called with `opaque` as its\n+    argument before the test returns (even if the test has failed).\n+    If a global cleanup method is also available, the local cleanup will be\n+    called first, and then the global.\n+\n+-   `cl_assert_equal_i(int,int)`: Verify that two integer values are equal.\n+    The advantage of this over a simple `cl_assert` is that it will format\n+    a much nicer error report if the values are not equal.\n+\n+-   `cl_assert_equal_s(const char *,const char *)`: Verify that two strings\n+    are equal.  The expected value can also be NULL and this will correctly\n+    test for that.\n+\n+-   `cl_fixture_sandbox(const char *)`: Sets up a sandbox for a fixture\n+    so that you can mutate the file directly.\n+\n+-   `cl_fixture_cleanup(const char *)`: Tears down the previous fixture\n+    sandbox.\n+\n+-   `cl_fixture(const char *)`: Gets the full path to a fixture file.\n+\n+Please do note that these methods are *always* available whilst running a\n+test, even when calling auxiliary/static functions inside the same file.\n+\n+It's strongly encouraged to perform test assertions in auxiliary methods,\n+instead of returning error values. This is considered good Clar style.\n+\n+Style Example:\n+\n+~~~~ c\n+/*\n+ * Bad style: auxiliary functions return an error code\n+ */\n+\n+static int check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    if (aux == NULL)\n+        return -1;\n+\n+    return strcmp(my_function(aux), str) == 0 ? 0 : -1;\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    cl_must_pass_(\n+        check_string(\"foo\"),\n+        \"String differs after processing\"\n+    );\n+\n+    cl_must_pass_(\n+        check_string(\"bar\"),\n+        \"String differs after processing\"\n+    );\n+}\n+~~~~\n+\n+~~~~ c\n+/*\n+ * Good style: auxiliary functions perform assertions\n+ */\n+\n+static void check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    cl_assert_(\n+        aux != NULL,\n+        \"String processing failed\"\n+    );\n+\n+    cl_assert_(\n+        strcmp(my_function(aux), str) == 0,\n+        \"String differs after processing\"\n+    );\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    check_string(\"foo\");\n+    check_string(\"bar\");\n+}\n+~~~~\n+\n+About Clar\n+==========\n+\n+Clar has been written from scratch by [Vicent Martí](https://github.com/vmg),\n+to replace the old testing framework in [libgit2][libgit2].\n+\n+Do you know what languages are *in* on the SF startup scene? Node.js *and*\n+Latin.  Follow [@vmg](https://www.twitter.com/vmg) on Twitter to\n+receive more lessons on word etymology. You can be hip too.\n+\n+\n+[libgit2]: https://github.com/libgit2/libgit2\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nnew file mode 100644\nindex 0000000000..3fc2c76815\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.c\n@@ -0,0 +1,842 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#include <assert.h>\n+#include <setjmp.h>\n+#include <stdlib.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <math.h>\n+#include <stdarg.h>\n+#include <wchar.h>\n+#include <time.h>\n+\n+/* required for sandboxing */\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#ifdef _WIN32\n+#\tinclude <windows.h>\n+#\tinclude <io.h>\n+#\tinclude <shellapi.h>\n+#\tinclude <direct.h>\n+\n+#\tdefine _MAIN_CC __cdecl\n+\n+#\tifndef stat\n+#\t\tdefine stat(path, st) _stat(path, st)\n+#\tendif\n+#\tifndef mkdir\n+#\t\tdefine mkdir(path, mode) _mkdir(path)\n+#\tendif\n+#\tifndef chdir\n+#\t\tdefine chdir(path) _chdir(path)\n+#\tendif\n+#\tifndef access\n+#\t\tdefine access(path, mode) _access(path, mode)\n+#\tendif\n+#\tifndef strdup\n+#\t\tdefine strdup(str) _strdup(str)\n+#\tendif\n+#\tifndef strcasecmp\n+#\t\tdefine strcasecmp(a,b) _stricmp(a,b)\n+#\tendif\n+\n+#\tifndef __MINGW32__\n+#\t\tpragma comment(lib, \"shell32\")\n+#\t\tifndef strncpy\n+#\t\t\tdefine strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)\n+#\t\tendif\n+#\t\tifndef W_OK\n+#\t\t\tdefine W_OK 02\n+#\t\tendif\n+#\t\tifndef S_ISDIR\n+#\t\t\tdefine S_ISDIR(x) ((x & _S_IFDIR) != 0)\n+#\t\tendif\n+#\t\tdefine p_snprintf(buf,sz,fmt,...) _snprintf_s(buf,sz,_TRUNCATE,fmt,__VA_ARGS__)\n+#\telse\n+#\t\tdefine p_snprintf snprintf\n+#\tendif\n+\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"Iu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"Ix\"\n+#\tendif\n+\n+#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+\ttypedef struct stat STAT_T;\n+#\telse\n+\ttypedef struct _stat STAT_T;\n+#\tendif\n+#else\n+#\tinclude <sys/wait.h> /* waitpid(2) */\n+#\tinclude <unistd.h>\n+#\tdefine _MAIN_CC\n+#\tdefine p_snprintf snprintf\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"zu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"zx\"\n+#\tendif\n+\ttypedef struct stat STAT_T;\n+#endif\n+\n+#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n+\n+#include \"clar.h\"\n+\n+static void fs_rm(const char *_source);\n+static void fs_copy(const char *_source, const char *dest);\n+\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name);\n+#endif\n+\n+struct clar_error {\n+\tconst char *file;\n+\tconst char *function;\n+\tsize_t line_number;\n+\tconst char *error_msg;\n+\tchar *description;\n+\n+\tstruct clar_error *next;\n+};\n+\n+struct clar_explicit {\n+\tsize_t suite_idx;\n+\tconst char *filter;\n+\n+\tstruct clar_explicit *next;\n+};\n+\n+struct clar_report {\n+\tconst char *test;\n+\tint test_number;\n+\tconst char *suite;\n+\n+\tenum cl_test_status status;\n+\ttime_t start;\n+\tdouble elapsed;\n+\n+\tstruct clar_error *errors;\n+\tstruct clar_error *last_error;\n+\n+\tstruct clar_report *next;\n+};\n+\n+struct clar_summary {\n+\tconst char *filename;\n+\tFILE *fp;\n+};\n+\n+static struct {\n+\tenum cl_test_status test_status;\n+\n+\tconst char *active_test;\n+\tconst char *active_suite;\n+\n+\tint total_skipped;\n+\tint total_errors;\n+\n+\tint tests_ran;\n+\tint suites_ran;\n+\n+\tenum cl_output_format output_format;\n+\n+\tint report_errors_only;\n+\tint exit_on_error;\n+\tint verbosity;\n+\n+\tint write_summary;\n+\tchar *summary_filename;\n+\tstruct clar_summary *summary;\n+\n+\tstruct clar_explicit *explicit;\n+\tstruct clar_explicit *last_explicit;\n+\n+\tstruct clar_report *reports;\n+\tstruct clar_report *last_report;\n+\n+\tvoid (*local_cleanup)(void *);\n+\tvoid *local_cleanup_payload;\n+\n+\tjmp_buf trampoline;\n+\tint trampoline_enabled;\n+\n+\tcl_trace_cb *pfn_trace_cb;\n+\tvoid *trace_payload;\n+\n+} _clar;\n+\n+struct clar_func {\n+\tconst char *name;\n+\tvoid (*ptr)(void);\n+};\n+\n+struct clar_suite {\n+\tconst char *name;\n+\tstruct clar_func initialize;\n+\tstruct clar_func cleanup;\n+\tconst struct clar_func *tests;\n+\tsize_t test_count;\n+\tint enabled;\n+};\n+\n+/* From clar_print_*.c */\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n+static void clar_print_onsuite(const char *suite_name, int suite_index);\n+static void clar_print_onabort(const char *msg, ...);\n+\n+/* From clar_sandbox.c */\n+static void clar_unsandbox(void);\n+static int clar_sandbox(void);\n+\n+/* From summary.h */\n+static struct clar_summary *clar_summary_init(const char *filename);\n+static int clar_summary_shutdown(struct clar_summary *fp);\n+\n+/* Load the declarations for the test suite */\n+#include \"clar.suite\"\n+\n+\n+#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n+\t} while (0)\n+\n+void cl_trace_register(cl_trace_cb *cb, void *payload)\n+{\n+\t_clar.pfn_trace_cb = cb;\n+\t_clar.trace_payload = payload;\n+}\n+\n+\n+/* Core test functions */\n+static void\n+clar_report_errors(struct clar_report *report)\n+{\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (error = report->errors; error; error = error->next)\n+\t\tclar_print_error(i++, _clar.last_report, error);\n+}\n+\n+static void\n+clar_report_all(void)\n+{\n+\tstruct clar_report *report;\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (report = _clar.reports; report; report = report->next) {\n+\t\tif (report->status != CL_TEST_FAILURE)\n+\t\t\tcontinue;\n+\n+\t\tfor (error = report->errors; error; error = error->next)\n+\t\t\tclar_print_error(i++, report, error);\n+\t}\n+}\n+\n+#ifdef WIN32\n+# define clar_time DWORD\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\t*out = GetTickCount();\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)*end - (double)*start) / 1000;\n+}\n+#else\n+# include <sys/time.h>\n+\n+# define clar_time struct timeval\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\tstruct timezone tz;\n+\n+\tgettimeofday(out, &tz);\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n+\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n+}\n+#endif\n+\n+static void\n+clar_run_test(\n+\tconst struct clar_suite *suite,\n+\tconst struct clar_func *test,\n+\tconst struct clar_func *initialize,\n+\tconst struct clar_func *cleanup)\n+{\n+\tclar_time start, end;\n+\n+\t_clar.trampoline_enabled = 1;\n+\n+\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n+\n+\t_clar.last_report->start = time(NULL);\n+\tclar_time_now(&start);\n+\n+\tif (setjmp(_clar.trampoline) == 0) {\n+\t\tif (initialize->ptr != NULL)\n+\t\t\tinitialize->ptr();\n+\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n+\t\ttest->ptr();\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n+\t}\n+\n+\tclar_time_now(&end);\n+\n+\t_clar.trampoline_enabled = 0;\n+\n+\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n+\t\t_clar.last_report->status = CL_TEST_OK;\n+\n+\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n+\n+\tif (_clar.local_cleanup != NULL)\n+\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n+\n+\tif (cleanup->ptr != NULL)\n+\t\tcleanup->ptr();\n+\n+\tCL_TRACE(CL_TRACE__TEST__END);\n+\n+\t_clar.tests_ran++;\n+\n+\t/* remove any local-set cleanup methods */\n+\t_clar.local_cleanup = NULL;\n+\t_clar.local_cleanup_payload = NULL;\n+\n+\tif (_clar.report_errors_only) {\n+\t\tclar_report_errors(_clar.last_report);\n+\t} else {\n+\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n+\t}\n+}\n+\n+static void\n+clar_run_suite(const struct clar_suite *suite, const char *filter)\n+{\n+\tconst struct clar_func *test = suite->tests;\n+\tsize_t i, matchlen;\n+\tstruct clar_report *report;\n+\tint exact = 0;\n+\n+\tif (!suite->enabled)\n+\t\treturn;\n+\n+\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\treturn;\n+\n+\tif (!_clar.report_errors_only)\n+\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n+\n+\t_clar.active_suite = suite->name;\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n+\n+\tif (filter) {\n+\t\tsize_t suitelen = strlen(suite->name);\n+\t\tmatchlen = strlen(filter);\n+\t\tif (matchlen <= suitelen) {\n+\t\t\tfilter = NULL;\n+\t\t} else {\n+\t\t\tfilter += suitelen;\n+\t\t\twhile (*filter == ':')\n+\t\t\t\t++filter;\n+\t\t\tmatchlen = strlen(filter);\n+\n+\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n+\t\t\t\texact = 1;\n+\t\t\t\tmatchlen--;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (i = 0; i < suite->test_count; ++i) {\n+\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n+\t\t\tcontinue;\n+\n+\t\tif (exact && strlen(test[i].name) != matchlen)\n+\t\t\tcontinue;\n+\n+\t\t_clar.active_test = test[i].name;\n+\n+\t\treport = calloc(1, sizeof(struct clar_report));\n+\t\treport->suite = _clar.active_suite;\n+\t\treport->test = _clar.active_test;\n+\t\treport->test_number = _clar.tests_ran;\n+\t\treport->status = CL_TEST_NOTRUN;\n+\n+\t\tif (_clar.reports == NULL)\n+\t\t\t_clar.reports = report;\n+\n+\t\tif (_clar.last_report != NULL)\n+\t\t\t_clar.last_report->next = report;\n+\n+\t\t_clar.last_report = report;\n+\n+\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n+\n+\t\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\t\treturn;\n+\t}\n+\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_END);\n+}\n+\n+static void\n+clar_usage(const char *arg)\n+{\n+\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n+\tprintf(\"Options:\\n\");\n+\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n+\tprintf(\"  -iname        Include the suite with `name`\\n\");\n+\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n+\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n+\tprintf(\"  -q            Only report tests that had an error\\n\");\n+\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n+\tprintf(\"  -t            Display results in tap format\\n\");\n+\tprintf(\"  -l            Print suite names\\n\");\n+\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n+\texit(-1);\n+}\n+\n+static void\n+clar_parse_args(int argc, char **argv)\n+{\n+\tint i;\n+\n+\t/* Verify options before execute */\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tif (argument[0] != '-' || argument[1] == '\\0'\n+\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n+\t\t\tclar_usage(argv[0]);\n+\t\t}\n+\t}\n+\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tswitch (argument[1]) {\n+\t\tcase 's':\n+\t\tcase 'i':\n+\t\tcase 'x': { /* given suite name */\n+\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n+\t\t\tchar action = argument[1];\n+\t\t\tsize_t j, arglen, suitelen, cmplen;\n+\n+\t\t\targument += offset;\n+\t\t\targlen = strlen(argument);\n+\n+\t\t\tif (arglen == 0)\n+\t\t\t\tclar_usage(argv[0]);\n+\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n+\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n+\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n+\n+\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n+\t\t\t\t\tint exact = (arglen >= suitelen);\n+\n+\t\t\t\t\t/* Do we have a real suite prefix separated by a\n+\t\t\t\t\t * trailing '::' or just a matching substring? */\n+\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n+\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n+\t\t\t\t\t    continue;\n+\n+\t\t\t\t\t++found;\n+\n+\t\t\t\t\tif (!exact)\n+\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n+\n+\t\t\t\t\tswitch (action) {\n+\t\t\t\t\tcase 's': {\n+\t\t\t\t\t\tstruct clar_explicit *explicit =\n+\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n+\t\t\t\t\t\tassert(explicit);\n+\n+\t\t\t\t\t\texplicit->suite_idx = j;\n+\t\t\t\t\t\texplicit->filter = argument;\n+\n+\t\t\t\t\t\tif (_clar.explicit == NULL)\n+\t\t\t\t\t\t\t_clar.explicit = explicit;\n+\n+\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n+\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n+\n+\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n+\t\t\t\t\t\t_clar.last_explicit = explicit;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n+\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif (exact)\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (!found) {\n+\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n+\t\t\t\texit(-1);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase 'q':\n+\t\t\t_clar.report_errors_only = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 'Q':\n+\t\t\t_clar.exit_on_error = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 't':\n+\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n+\t\t\tbreak;\n+\n+\t\tcase 'l': {\n+\t\t\tsize_t j;\n+\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n+\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n+\n+\t\t\texit(0);\n+\t\t}\n+\n+\t\tcase 'v':\n+\t\t\t_clar.verbosity++;\n+\t\t\tbreak;\n+\n+\t\tcase 'r':\n+\t\t\t_clar.write_summary = 1;\n+\t\t\tfree(_clar.summary_filename);\n+\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tassert(!\"Unexpected commandline argument!\");\n+\t\t}\n+\t}\n+}\n+\n+void\n+clar_test_init(int argc, char **argv)\n+{\n+\tconst char *summary_env;\n+\n+\tif (argc > 1)\n+\t\tclar_parse_args(argc, argv);\n+\n+\tclar_print_init(\n+\t\t(int)_clar_callback_count,\n+\t\t(int)_clar_suite_count,\n+\t\t\"\"\n+\t);\n+\n+\tif (!_clar.summary_filename &&\n+\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n+\t\t_clar.write_summary = 1;\n+\t\t_clar.summary_filename = strdup(summary_env);\n+\t}\n+\n+\tif (_clar.write_summary && !_clar.summary_filename)\n+\t\t_clar.summary_filename = strdup(\"summary.xml\");\n+\n+\tif (_clar.write_summary &&\n+\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n+\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tif (clar_sandbox() < 0) {\n+\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n+\t\texit(-1);\n+\t}\n+}\n+\n+int\n+clar_test_run(void)\n+{\n+\tsize_t i;\n+\tstruct clar_explicit *explicit;\n+\n+\tif (_clar.explicit) {\n+\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n+\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n+\t} else {\n+\t\tfor (i = 0; i < _clar_suite_count; ++i)\n+\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n+\t}\n+\n+\treturn _clar.total_errors;\n+}\n+\n+void\n+clar_test_shutdown(void)\n+{\n+\tstruct clar_explicit *explicit, *explicit_next;\n+\tstruct clar_report *report, *report_next;\n+\n+\tclar_print_shutdown(\n+\t\t_clar.tests_ran,\n+\t\t(int)_clar_suite_count,\n+\t\t_clar.total_errors\n+\t);\n+\n+\tclar_unsandbox();\n+\n+\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n+\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n+\t\texplicit_next = explicit->next;\n+\t\tfree(explicit);\n+\t}\n+\n+\tfor (report = _clar.reports; report; report = report_next) {\n+\t\treport_next = report->next;\n+\t\tfree(report);\n+\t}\n+\n+\tfree(_clar.summary_filename);\n+}\n+\n+int\n+clar_test(int argc, char **argv)\n+{\n+\tint errors;\n+\n+\tclar_test_init(argc, argv);\n+\terrors = clar_test_run();\n+\tclar_test_shutdown();\n+\n+\treturn errors;\n+}\n+\n+static void abort_test(void)\n+{\n+\tif (!_clar.trampoline_enabled) {\n+\t\tclar_print_onabort(\n+\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n+\t\tclar_report_errors(_clar.last_report);\n+\t\texit(-1);\n+\t}\n+\n+\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n+\tlongjmp(_clar.trampoline, -1);\n+}\n+\n+void clar__skip(void)\n+{\n+\t_clar.last_report->status = CL_TEST_SKIP;\n+\t_clar.total_skipped++;\n+\tabort_test();\n+}\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n+\n+\tif (_clar.last_report->errors == NULL)\n+\t\t_clar.last_report->errors = error;\n+\n+\tif (_clar.last_report->last_error != NULL)\n+\t\t_clar.last_report->last_error->next = error;\n+\n+\t_clar.last_report->last_error = error;\n+\n+\terror->file = file;\n+\terror->function = function;\n+\terror->line_number = line;\n+\terror->error_msg = error_msg;\n+\n+\tif (description != NULL)\n+\t\terror->description = strdup(description);\n+\n+\t_clar.total_errors++;\n+\t_clar.last_report->status = CL_TEST_FAILURE;\n+\n+\tif (should_abort)\n+\t\tabort_test();\n+}\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tif (condition)\n+\t\treturn;\n+\n+\tclar__fail(file, function, line, error_msg, description, should_abort);\n+}\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...)\n+{\n+\tva_list args;\n+\tchar buf[4096];\n+\tint is_equal = 1;\n+\n+\tva_start(args, fmt);\n+\n+\tif (!strcmp(\"%s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n+\t\t\t\t\ts1, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n+\t\t\t\t\tlen, s1, len, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n+\t\t\t\t\twcs1, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n+\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n+\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n+\t\tis_equal = (sz1 == sz2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%p\", fmt)) {\n+\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n+\t\tis_equal = (p1 == p2);\n+\t\tif (!is_equal)\n+\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n+\t}\n+\telse {\n+\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n+\t\tis_equal = (i1 == i2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n+\t\t}\n+\t}\n+\n+\tva_end(args);\n+\n+\tif (!is_equal)\n+\t\tclar__fail(file, function, line, err, buf, should_abort);\n+}\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n+{\n+\t_clar.local_cleanup = cleanup;\n+\t_clar.local_cleanup_payload = opaque;\n+}\n+\n+#include \"clar/sandbox.h\"\n+#include \"clar/fixtures.h\"\n+#include \"clar/fs.h\"\n+#include \"clar/print.h\"\n+#include \"clar/summary.h\"\ndiff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\nnew file mode 100644\nindex 0000000000..8c22382bd5\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.h\n@@ -0,0 +1,173 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST_H__\n+#define __CLAR_TEST_H__\n+\n+#include <stdlib.h>\n+\n+enum cl_test_status {\n+\tCL_TEST_OK,\n+\tCL_TEST_FAILURE,\n+\tCL_TEST_SKIP,\n+\tCL_TEST_NOTRUN,\n+};\n+\n+enum cl_output_format {\n+\tCL_OUTPUT_CLAP,\n+\tCL_OUTPUT_TAP,\n+};\n+\n+/** Setup clar environment */\n+void clar_test_init(int argc, char *argv[]);\n+int clar_test_run(void);\n+void clar_test_shutdown(void);\n+\n+/** One shot setup & run */\n+int clar_test(int argc, char *argv[]);\n+\n+const char *clar_sandbox_path(void);\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n+void cl_fs_cleanup(void);\n+\n+/**\n+ * cl_trace_* is a hook to provide a simple global tracing\n+ * mechanism.\n+ *\n+ * The goal here is to let main() provide clar-proper\n+ * with a callback to optionally write log info for\n+ * test operations into the same stream used by their\n+ * actual tests.  This would let them print test names\n+ * and maybe performance data as they choose.\n+ *\n+ * The goal is NOT to alter the flow of control or to\n+ * override test selection/skipping.  (So the callback\n+ * does not return a value.)\n+ *\n+ * The goal is NOT to duplicate the existing\n+ * pass/fail/skip reporting.  (So the callback\n+ * does not accept a status/errorcode argument.)\n+ *\n+ */\n+typedef enum cl_trace_event {\n+\tCL_TRACE__SUITE_BEGIN,\n+\tCL_TRACE__SUITE_END,\n+\tCL_TRACE__TEST__BEGIN,\n+\tCL_TRACE__TEST__END,\n+\tCL_TRACE__TEST__RUN_BEGIN,\n+\tCL_TRACE__TEST__RUN_END,\n+\tCL_TRACE__TEST__LONGJMP,\n+} cl_trace_event;\n+\n+typedef void (cl_trace_cb)(\n+\tcl_trace_event ev,\n+\tconst char *suite_name,\n+\tconst char *test_name,\n+\tvoid *payload);\n+\n+/**\n+ * Register a callback into CLAR to send global trace events.\n+ * Pass NULL to disable.\n+ */\n+void cl_trace_register(cl_trace_cb *cb, void *payload);\n+\n+\n+#ifdef CLAR_FIXTURE_PATH\n+const char *cl_fixture(const char *fixture_name);\n+void cl_fixture_sandbox(const char *fixture_name);\n+void cl_fixture_cleanup(const char *fixture_name);\n+const char *cl_fixture_basename(const char *fixture_name);\n+#endif\n+\n+/**\n+ * Assertion macros with explicit error message\n+ */\n+#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n+#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n+#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n+\n+/**\n+ * Check macros with explicit error message\n+ */\n+#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n+#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n+#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n+\n+/**\n+ * Assertion macros with no error message\n+ */\n+#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n+#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n+#define cl_assert(expr) cl_assert_(expr, NULL)\n+\n+/**\n+ * Check macros with no error message\n+ */\n+#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n+#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n+#define cl_check(expr) cl_check_(expr, NULL)\n+\n+/**\n+ * Forced failure/warning\n+ */\n+#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n+#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n+\n+#define cl_skip() clar__skip()\n+\n+/**\n+ * Typed assertion macros\n+ */\n+#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n+#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n+\n+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n+\n+#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n+#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n+\n+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+\n+#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n+#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n+#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n+\n+#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n+\n+#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n+\n+void clar__skip(void);\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...);\n+\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\nnew file mode 100644\nindex 0000000000..6ec6423484\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fixtures.h\n@@ -0,0 +1,50 @@\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name)\n+{\n+\tstatic char _path[4096];\n+\tsize_t root_len;\n+\n+\troot_len = strlen(base);\n+\tstrncpy(_path, base, sizeof(_path));\n+\n+\tif (_path[root_len - 1] != '/')\n+\t\t_path[root_len++] = '/';\n+\n+\tif (fixture_name[0] == '/')\n+\t\tfixture_name++;\n+\n+\tstrncpy(_path + root_len,\n+\t\tfixture_name,\n+\t\tsizeof(_path) - root_len);\n+\n+\treturn _path;\n+}\n+\n+const char *cl_fixture(const char *fixture_name)\n+{\n+\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n+}\n+\n+void cl_fixture_sandbox(const char *fixture_name)\n+{\n+\tfs_copy(cl_fixture(fixture_name), _clar_path);\n+}\n+\n+const char *cl_fixture_basename(const char *fixture_name)\n+{\n+\tconst char *p;\n+\n+\tfor (p = fixture_name; *p; p++) {\n+\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n+\t\t\tfixture_name = p+1;\n+\t}\n+\n+\treturn fixture_name;\n+}\n+\n+void cl_fixture_cleanup(const char *fixture_name)\n+{\n+\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nnew file mode 100644\nindex 0000000000..a6eda5e5dc\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -0,0 +1,522 @@\n+/*\n+ * By default, use a read/write loop to copy files on POSIX systems.\n+ * On Linux, use sendfile by default as it's slightly faster.  On\n+ * macOS, we avoid fcopyfile by default because it's slightly slower.\n+ */\n+#undef USE_FCOPYFILE\n+#define USE_SENDFILE 1\n+\n+#ifdef _WIN32\n+\n+#ifdef CLAR_WIN32_LONGPATHS\n+# define CLAR_MAX_PATH 4096\n+#else\n+# define CLAR_MAX_PATH MAX_PATH\n+#endif\n+\n+#define RM_RETRY_COUNT\t5\n+#define RM_RETRY_DELAY\t10\n+\n+#ifdef __MINGW32__\n+\n+/* These security-enhanced functions are not available\n+ * in MinGW, so just use the vanilla ones */\n+#define wcscpy_s(a, b, c) wcscpy((a), (c))\n+#define wcscat_s(a, b, c) wcscat((a), (c))\n+\n+#endif /* __MINGW32__ */\n+\n+static int\n+fs__dotordotdot(WCHAR *_tocheck)\n+{\n+\treturn _tocheck[0] == '.' &&\n+\t\t(_tocheck[1] == '\\0' ||\n+\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n+}\n+\n+static int\n+fs_rmdir_rmdir(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\n+\twhile (!RemoveDirectoryW(_wpath)) {\n+\t\t/* Only retry when we have retries remaining, and the\n+\t\t * error was ERROR_DIR_NOT_EMPTY. */\n+\t\tif (retries++ > RM_RETRY_COUNT ||\n+\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n+\t\t\treturn -1;\n+\n+\t\t/* Give whatever has a handle to a child item some time\n+\t\t * to release it before trying again */\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void translate_path(WCHAR *path, size_t path_size)\n+{\n+    size_t path_len, i;\n+\n+    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n+\treturn;\n+\n+    path_len = wcslen(path);\n+    cl_assert(path_size > path_len + 4);\n+\n+    for (i = path_len; i > 0; i--) {\n+\tWCHAR c = path[i - 1];\n+\n+\tif (c == L'/')\n+\t    path[i + 3] = L'\\\\';\n+\telse\n+\t    path[i + 3] = path[i - 1];\n+    }\n+\n+    path[0] = L'\\\\';\n+    path[1] = L'\\\\';\n+    path[2] = L'?';\n+    path[3] = L'\\\\';\n+    path[path_len + 4] = L'\\0';\n+}\n+\n+static void\n+fs_rmdir_helper(WCHAR *_wsource)\n+{\n+\tWCHAR buffer[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buffer_prefix_len;\n+\n+\t/* Set up the buffer and capture the length */\n+\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n+\ttranslate_path(buffer, CLAR_MAX_PATH);\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n+\tbuffer_prefix_len = wcslen(buffer);\n+\n+\t/* FindFirstFile needs a wildcard to match multiple items */\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buffer, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_rmdir_helper(buffer);\n+\t\telse {\n+\t\t\t/* If set, the +R bit must be cleared before deleting */\n+\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n+\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\t\tcl_assert(DeleteFileW(buffer));\n+\t\t}\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+\n+\t/* Now that the directory is empty, remove it */\n+\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n+}\n+\n+static int\n+fs_rm_wait(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\tDWORD last_error;\n+\n+\tdo {\n+\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n+\t\t\tlast_error = GetLastError();\n+\t\telse\n+\t\t\tlast_error = ERROR_SUCCESS;\n+\n+\t\t/* Is the item gone? */\n+\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n+\t\t\tERROR_PATH_NOT_FOUND == last_error)\n+\t\t\treturn 0;\n+\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n+\t}\n+\twhile (retries++ <= RM_RETRY_COUNT);\n+\n+\treturn -1;\n+}\n+\n+static void\n+fs_rm(const char *_source)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH];\n+\tDWORD attrs;\n+\n+\t/* The input path is UTF-8. Convert it to wide characters\n+\t * for use with the Windows API */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1, /* Indicates NULL termination */\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\n+\t/* Does the item exist? If not, we have no work to do */\n+\tattrs = GetFileAttributesW(wsource);\n+\n+\tif (INVALID_FILE_ATTRIBUTES == attrs)\n+\t\treturn;\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n+\t\tfs_rmdir_helper(wsource);\n+\telse {\n+\t\t/* The item is a file. Strip the +R bit */\n+\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n+\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\tcl_assert(DeleteFileW(wsource));\n+\t}\n+\n+\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n+\tcl_assert(0 == fs_rm_wait(wsource));\n+}\n+\n+static void\n+fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n+{\n+\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n+\n+\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_source, CLAR_MAX_PATH);\n+\tbuf_source_prefix_len = wcslen(buf_source);\n+\n+\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n+\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n+\tbuf_dest_prefix_len = wcslen(buf_dest);\n+\n+\t/* Get an enumerator for the items in the source. */\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buf_source, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\t/* Create the target directory. */\n+\tcl_assert(CreateDirectoryW(_wdest, NULL));\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n+\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_copydir_helper(buf_source, buf_dest);\n+\t\telse\n+\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+}\n+\n+static void\n+fs_copy(const char *_source, const char *_dest)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n+\tDWORD source_attrs, dest_attrs;\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\n+\t/* The input paths are UTF-8. Convert them to wide characters\n+\t * for use with the Windows API. */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1,\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_dest,\n+\t\t\t\t-1,\n+\t\t\t\twdest,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\ttranslate_path(wdest, CLAR_MAX_PATH);\n+\n+\t/* Check the source for existence */\n+\tsource_attrs = GetFileAttributesW(wsource);\n+\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n+\n+\t/* Check the target for existence */\n+\tdest_attrs = GetFileAttributesW(wdest);\n+\n+\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n+\t\t/* Target exists; append last path part of source to target.\n+\t\t * Use FindFirstFile to parse the path */\n+\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n+\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n+\t\tFindClose(find_handle);\n+\n+\t\t/* Check the new target for existence */\n+\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n+\t}\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n+\t\tfs_copydir_helper(wsource, wdest);\n+\telse\n+\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+#ifdef CLAR_FIXTURE_PATH\n+\tfs_rm(fixture_path(_clar_path, \"*\"));\n+#endif\n+}\n+\n+#else\n+\n+#include <errno.h>\n+#include <string.h>\n+#include <limits.h>\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#if defined(__linux__)\n+# include <sys/sendfile.h>\n+#endif\n+\n+#if defined(__APPLE__)\n+# include <copyfile.h>\n+#endif\n+\n+static void basename_r(const char **out, int *out_len, const char *in)\n+{\n+\tsize_t in_len = strlen(in), start_pos;\n+\n+\tfor (in_len = strlen(in); in_len; in_len--) {\n+\t\tif (in[in_len - 1] != '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tfor (start_pos = in_len; start_pos; start_pos--) {\n+\t\tif (in[start_pos - 1] == '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tcl_assert(in_len - start_pos < INT_MAX);\n+\n+\tif (in_len - start_pos > 0) {\n+\t\t*out = &in[start_pos];\n+\t\t*out_len = (in_len - start_pos);\n+\t} else {\n+\t\t*out = \"/\";\n+\t\t*out_len = 1;\n+\t}\n+}\n+\n+static char *joinpath(const char *dir, const char *base, int base_len)\n+{\n+\tchar *out;\n+\tint len;\n+\n+\tif (base_len == -1) {\n+\t\tsize_t bl = strlen(base);\n+\n+\t\tcl_assert(bl < INT_MAX);\n+\t\tbase_len = (int)bl;\n+\t}\n+\n+\tlen = strlen(dir) + base_len + 2;\n+\tcl_assert(len > 0);\n+\n+\tcl_assert(out = malloc(len));\n+\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n+\n+\treturn out;\n+}\n+\n+static void\n+fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n+{\n+\tDIR *source_dir;\n+\tstruct dirent *d;\n+\n+\tmkdir(dest, dest_mode);\n+\n+\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n+\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(source, d->d_name, -1);\n+\t\tfs_copy(child, dest);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\n+\tclosedir(source_dir);\n+}\n+\n+static void\n+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n+{\n+\tint in, out;\n+\n+\tcl_must_pass((in = open(source, O_RDONLY)));\n+\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n+\n+#if USE_FCOPYFILE && defined(__APPLE__)\n+\t((void)(source_len)); /* unused */\n+\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n+#elif USE_SENDFILE && defined(__linux__)\n+\t{\n+\t\tssize_t ret = 0;\n+\n+\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n+\t\t\tsource_len -= (size_t)ret;\n+\t\t}\n+\t\tcl_assert(ret >= 0);\n+\t}\n+#else\n+\t{\n+\t\tchar buf[131072];\n+\t\tssize_t ret;\n+\n+\t\t((void)(source_len)); /* unused */\n+\n+\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n+\t\t\tsize_t len = (size_t)ret;\n+\n+\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n+\t\t\t\tcl_assert(ret <= (ssize_t)len);\n+\t\t\t\tlen -= ret;\n+\t\t\t}\n+\t\t\tcl_assert(ret >= 0);\n+\t\t}\n+\t\tcl_assert(ret == 0);\n+\t}\n+#endif\n+\n+\tclose(in);\n+\tclose(out);\n+}\n+\n+static void\n+fs_copy(const char *source, const char *_dest)\n+{\n+\tchar *dbuf = NULL;\n+\tconst char *dest = NULL;\n+\tstruct stat source_st, dest_st;\n+\n+\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n+\n+\tif (lstat(_dest, &dest_st) == 0) {\n+\t\tconst char *base;\n+\t\tint base_len;\n+\n+\t\t/* Target exists and is directory; append basename */\n+\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n+\n+\t\tbasename_r(&base, &base_len, source);\n+\t\tcl_assert(base_len < INT_MAX);\n+\n+\t\tdbuf = joinpath(_dest, base, base_len);\n+\t\tdest = dbuf;\n+\t} else if (errno != ENOENT) {\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t} else {\n+\t\tdest = _dest;\n+\t}\n+\n+\tif (S_ISDIR(source_st.st_mode)) {\n+\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n+\t} else {\n+\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n+\t}\n+\n+\tfree(dbuf);\n+}\n+\n+static void\n+fs_rmdir_helper(const char *path)\n+{\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n+\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(path, d->d_name, -1);\n+\t\tfs_rm(child);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\tclosedir(dir);\n+\n+\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n+}\n+\n+static void\n+fs_rm(const char *path)\n+{\n+\tstruct stat st;\n+\n+\tif (lstat(path, &st)) {\n+\t\tif (errno == ENOENT)\n+\t\t\treturn;\n+\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t}\n+\n+\tif (S_ISDIR(st.st_mode)) {\n+\t\tfs_rmdir_helper(path);\n+\t} else {\n+\t\tcl_must_pass(unlink(path));\n+\t}\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+\tclar_unsandbox();\n+\tclar_sandbox();\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\nnew file mode 100644\nindex 0000000000..c17e2f693b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/print.h\n@@ -0,0 +1,211 @@\n+/* clap: clar protocol, the traditional clar output format */\n+\n+static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n+\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n+}\n+\n+static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"\\n\\n\");\n+\tclar_report_all();\n+}\n+\n+static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tprintf(\"  %d) Failure:\\n\", num);\n+\n+\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n+\t\treport->suite,\n+\t\treport->test,\n+\t\terror->file,\n+\t\terror->line_number);\n+\n+\tprintf(\"  %s\\n\", error->error_msg);\n+\n+\tif (error->description != NULL)\n+\t\tprintf(\"  %s\\n\", error->description);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tif (_clar.verbosity > 1) {\n+\t\tprintf(\"%s::%s: \", suite_name, test_name);\n+\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n+\t\t}\n+\t} else {\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\".\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n+\t\t}\n+\n+\t\tfflush(stdout);\n+\t}\n+}\n+\n+static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tif (_clar.verbosity == 1)\n+\t\tprintf(\"\\n%s\", suite_name);\n+\n+\t(void)suite_index;\n+}\n+\n+static void clar_print_clap_onabort(const char *fmt, va_list arg)\n+{\n+\tvfprintf(stderr, fmt, arg);\n+}\n+\n+/* tap: test anywhere protocol format */\n+\n+static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)suite_names;\n+\tprintf(\"TAP version 13\\n\");\n+}\n+\n+static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"1..%d\\n\", test_count);\n+}\n+\n+static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\t(void)num;\n+\t(void)report;\n+\t(void)error;\n+}\n+\n+static void print_escaped(const char *str)\n+{\n+\tchar *c;\n+\n+\twhile ((c = strchr(str, '\\'')) != NULL) {\n+\t\tprintf(\"%.*s\", (int)(c - str), str);\n+\t\tprintf(\"''\");\n+\t\tstr = c + 1;\n+\t}\n+\n+\tprintf(\"%s\", str);\n+}\n+\n+static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tconst struct clar_error *error = _clar.last_report->errors;\n+\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tswitch(status) {\n+\tcase CL_TEST_OK:\n+\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\tcase CL_TEST_FAILURE:\n+\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\n+\t\tprintf(\"    ---\\n\");\n+\t\tprintf(\"    reason: |\\n\");\n+\t\tprintf(\"      %s\\n\", error->error_msg);\n+\n+\t\tif (error->description)\n+\t\t\tprintf(\"      %s\\n\", error->description);\n+\n+\t\tprintf(\"    at:\\n\");\n+\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n+\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n+\t\tprintf(\"      function: '%s'\\n\", error->function);\n+\t\tprintf(\"    ---\\n\");\n+\n+\t\tbreak;\n+\tcase CL_TEST_SKIP:\n+\tcase CL_TEST_NOTRUN:\n+\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\t}\n+\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n+}\n+\n+static void clar_print_tap_onabort(const char *fmt, va_list arg)\n+{\n+\tprintf(\"Bail out! \");\n+\tvprintf(fmt, arg);\n+\tfflush(stdout);\n+}\n+\n+/* indirection between protocol output selection */\n+\n+#define PRINT(FN, ...) do { \\\n+\t\tswitch (_clar.output_format) { \\\n+\t\t\tcase CL_OUTPUT_CLAP: \\\n+\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tcase CL_OUTPUT_TAP: \\\n+\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tdefault: \\\n+\t\t\t\tabort(); \\\n+\t\t} \\\n+\t} while (0)\n+\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\tPRINT(init, test_count, suite_count, suite_names);\n+}\n+\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\tPRINT(shutdown, test_count, suite_count, error_count);\n+}\n+\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tPRINT(error, num, report, error);\n+}\n+\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tPRINT(ontest, suite_name, test_name, test_number, status);\n+}\n+\n+static void clar_print_onsuite(const char *suite_name, int suite_index)\n+{\n+\tPRINT(onsuite, suite_name, suite_index);\n+}\n+\n+static void clar_print_onabort(const char *msg, ...)\n+{\n+\tva_list argp;\n+\tva_start(argp, msg);\n+\tPRINT(onabort, msg, argp);\n+\tva_end(argp);\n+}\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nnew file mode 100644\nindex 0000000000..0ba1479620\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -0,0 +1,154 @@\n+#ifdef __APPLE__\n+#include <sys/syslimits.h>\n+#endif\n+\n+static char _clar_path[4096 + 1];\n+\n+static int\n+is_valid_tmp_path(const char *path)\n+{\n+\tSTAT_T st;\n+\n+\tif (stat(path, &st) != 0)\n+\t\treturn 0;\n+\n+\tif (!S_ISDIR(st.st_mode))\n+\t\treturn 0;\n+\n+\treturn (access(path, W_OK) == 0);\n+}\n+\n+static int\n+find_tmp_path(char *buffer, size_t length)\n+{\n+#ifndef _WIN32\n+\tstatic const size_t var_count = 5;\n+\tstatic const char *env_vars[] = {\n+\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n+ \t};\n+\n+ \tsize_t i;\n+\n+\tfor (i = 0; i < var_count; ++i) {\n+\t\tconst char *env = getenv(env_vars[i]);\n+\t\tif (!env)\n+\t\t\tcontinue;\n+\n+\t\tif (is_valid_tmp_path(env)) {\n+#ifdef __APPLE__\n+\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n+\t\t\t\treturn 0;\n+#endif\n+\t\t\tstrncpy(buffer, env, length - 1);\n+\t\t\tbuffer[length - 1] = '\\0';\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\t/* If the environment doesn't say anything, try to use /tmp */\n+\tif (is_valid_tmp_path(\"/tmp\")) {\n+#ifdef __APPLE__\n+\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n+\t\t\treturn 0;\n+#endif\n+\t\tstrncpy(buffer, \"/tmp\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+#else\n+\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n+\tif (env_len > 0 && env_len < (DWORD)length)\n+\t\treturn 0;\n+\n+\tif (GetTempPath((DWORD)length, buffer))\n+\t\treturn 0;\n+#endif\n+\n+\t/* This system doesn't like us, try to use the current directory */\n+\tif (is_valid_tmp_path(\".\")) {\n+\t\tstrncpy(buffer, \".\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static void clar_unsandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0')\n+\t\treturn;\n+\n+\tcl_must_pass(chdir(\"..\"));\n+\n+\tfs_rm(_clar_path);\n+}\n+\n+static int build_sandbox_path(void)\n+{\n+#ifdef CLAR_TMPDIR\n+\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n+#else\n+\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n+#endif\n+\n+\tsize_t len;\n+\n+\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n+\t\treturn -1;\n+\n+\tlen = strlen(_clar_path);\n+\n+#ifdef _WIN32\n+\t{ /* normalize path to POSIX forward slashes */\n+\t\tsize_t i;\n+\t\tfor (i = 0; i < len; ++i) {\n+\t\t\tif (_clar_path[i] == '\\\\')\n+\t\t\t\t_clar_path[i] = '/';\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (_clar_path[len - 1] != '/') {\n+\t\t_clar_path[len++] = '/';\n+\t}\n+\n+\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n+\n+#if defined(__MINGW32__)\n+\tif (_mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(_WIN32)\n+\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#else\n+\tif (mkdtemp(_clar_path) == NULL)\n+\t\treturn -1;\n+#endif\n+\n+\treturn 0;\n+}\n+\n+static int clar_sandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n+\t\treturn -1;\n+\n+\tif (chdir(_clar_path) != 0)\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const char *clar_sandbox_path(void)\n+{\n+\treturn _clar_path;\n+}\n+\ndiff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\nnew file mode 100644\nindex 0000000000..4dd352e28b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/summary.h\n@@ -0,0 +1,143 @@\n+\n+#include <stdio.h>\n+#include <time.h>\n+\n+static int clar_summary_close_tag(\n+    struct clar_summary *summary, const char *tag, int indent)\n+{\n+\tconst char *indt;\n+\n+\tif (indent == 0) indt = \"\";\n+\telse if (indent == 1) indt = \"\\t\";\n+\telse indt = \"\\t\\t\";\n+\n+\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n+}\n+\n+static int clar_summary_testsuites(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n+}\n+\n+static int clar_summary_testsuite(struct clar_summary *summary,\n+    int idn, const char *name, time_t timestamp,\n+    int test_count, int fail_count, int error_count)\n+{\n+\tstruct tm *tm = localtime(&timestamp);\n+\tchar iso_dt[20];\n+\n+\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n+\t\treturn -1;\n+\n+\treturn fprintf(summary->fp, \"\\t<testsuite\"\n+\t\t       \" id=\\\"%d\\\"\"\n+\t\t       \" name=\\\"%s\\\"\"\n+\t\t       \" hostname=\\\"localhost\\\"\"\n+\t\t       \" timestamp=\\\"%s\\\"\"\n+\t\t       \" tests=\\\"%d\\\"\"\n+\t\t       \" failures=\\\"%d\\\"\"\n+\t\t       \" errors=\\\"%d\\\">\\n\",\n+\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n+}\n+\n+static int clar_summary_testcase(struct clar_summary *summary,\n+    const char *name, const char *classname, double elapsed)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n+\t\tname, classname, elapsed);\n+}\n+\n+static int clar_summary_failure(struct clar_summary *summary,\n+    const char *type, const char *message, const char *desc)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n+\t    type, message, desc);\n+}\n+\n+static int clar_summary_skipped(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n+}\n+\n+struct clar_summary *clar_summary_init(const char *filename)\n+{\n+\tstruct clar_summary *summary;\n+\tFILE *fp;\n+\n+\tif ((fp = fopen(filename, \"w\")) == NULL) {\n+\t\tperror(\"fopen\");\n+\t\treturn NULL;\n+\t}\n+\n+\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n+\t\tperror(\"malloc\");\n+\t\tfclose(fp);\n+\t\treturn NULL;\n+\t}\n+\n+\tsummary->filename = filename;\n+\tsummary->fp = fp;\n+\n+\treturn summary;\n+}\n+\n+int clar_summary_shutdown(struct clar_summary *summary)\n+{\n+\tstruct clar_report *report;\n+\tconst char *last_suite = NULL;\n+\n+\tif (clar_summary_testsuites(summary) < 0)\n+\t\tgoto on_error;\n+\n+\treport = _clar.reports;\n+\twhile (report != NULL) {\n+\t\tstruct clar_error *error = report->errors;\n+\n+\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n+\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\n+\t\tlast_suite = report->suite;\n+\n+\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n+\n+\t\twhile (error != NULL) {\n+\t\t\tif (clar_summary_failure(summary, \"assert\",\n+\t\t\t    error->error_msg, error->description) < 0)\n+\t\t\t\tgoto on_error;\n+\n+\t\t\terror = error->next;\n+\t\t}\n+\n+\t\tif (report->status == CL_TEST_SKIP)\n+\t\t\tclar_summary_skipped(summary);\n+\n+\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n+\t\t\tgoto on_error;\n+\n+\t\treport = report->next;\n+\n+\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\t}\n+\n+\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n+\t    fclose(summary->fp) != 0)\n+\t\tgoto on_error;\n+\n+\tprintf(\"written summary file to %s\\n\", summary->filename);\n+\n+\tfree(summary);\n+\treturn 0;\n+\n+on_error:\n+\tfclose(summary->fp);\n+\tfree(summary);\n+\treturn -1;\n+}\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nnew file mode 100755\nindex 0000000000..931b4d60d9\n--- /dev/null\n+++ b/t/unit-tests/clar/generate.py\n@@ -0,0 +1,267 @@\n+#!/usr/bin/env python\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+from __future__ import with_statement\n+from string import Template\n+import re, fnmatch, os, sys, codecs, pickle\n+\n+class Module(object):\n+    class Template(object):\n+        def __init__(self, module):\n+            self.module = module\n+\n+        def _render_callback(self, cb):\n+            if not cb:\n+                return '    { NULL, NULL }'\n+            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n+\n+    class DeclarationTemplate(Template):\n+        def render(self):\n+            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n+\n+            for initializer in self.module.initializers:\n+                out += \"extern %s;\\n\" % initializer['declaration']\n+\n+            if self.module.cleanup:\n+                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n+\n+            return out\n+\n+    class CallbacksTemplate(Template):\n+        def render(self):\n+            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n+            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n+            out += \"\\n};\\n\"\n+            return out\n+\n+    class InfoTemplate(Template):\n+        def render(self):\n+            templates = []\n+\n+            initializers = self.module.initializers\n+            if len(initializers) == 0:\n+                initializers = [ None ]\n+\n+            for initializer in initializers:\n+                name = self.module.clean_name()\n+                if initializer and initializer['short_name'].startswith('initialize_'):\n+                    variant = initializer['short_name'][len('initialize_'):]\n+                    name += \" (%s)\" % variant.replace('_', ' ')\n+\n+                template = Template(\n+            r\"\"\"\n+    {\n+        \"${clean_name}\",\n+    ${initialize},\n+    ${cleanup},\n+        ${cb_ptr}, ${cb_count}, ${enabled}\n+    }\"\"\"\n+                ).substitute(\n+                    clean_name = name,\n+                    initialize = self._render_callback(initializer),\n+                    cleanup = self._render_callback(self.module.cleanup),\n+                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n+                    cb_count = len(self.module.callbacks),\n+                    enabled = int(self.module.enabled)\n+                )\n+                templates.append(template)\n+\n+            return ','.join(templates)\n+\n+    def __init__(self, name):\n+        self.name = name\n+\n+        self.mtime = None\n+        self.enabled = True\n+        self.modified = False\n+\n+    def clean_name(self):\n+        return self.name.replace(\"_\", \"::\")\n+\n+    def _skip_comments(self, text):\n+        SKIP_COMMENTS_REGEX = re.compile(\n+            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n+            re.DOTALL | re.MULTILINE)\n+\n+        def _replacer(match):\n+            s = match.group(0)\n+            return \"\" if s.startswith('/') else s\n+\n+        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n+\n+    def parse(self, contents):\n+        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n+\n+        contents = self._skip_comments(contents)\n+        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n+\n+        self.callbacks = []\n+        self.initializers = []\n+        self.cleanup = None\n+\n+        for (declaration, symbol, short_name) in regex.findall(contents):\n+            data = {\n+                \"short_name\" : short_name,\n+                \"declaration\" : declaration,\n+                \"symbol\" : symbol\n+            }\n+\n+            if short_name.startswith('initialize'):\n+                self.initializers.append(data)\n+            elif short_name == 'cleanup':\n+                self.cleanup = data\n+            else:\n+                self.callbacks.append(data)\n+\n+        return self.callbacks != []\n+\n+    def refresh(self, path):\n+        self.modified = False\n+\n+        try:\n+            st = os.stat(path)\n+\n+            # Not modified\n+            if st.st_mtime == self.mtime:\n+                return True\n+\n+            self.modified = True\n+            self.mtime = st.st_mtime\n+\n+            with codecs.open(path, encoding='utf-8') as fp:\n+                raw_content = fp.read()\n+\n+        except IOError:\n+            return False\n+\n+        return self.parse(raw_content)\n+\n+class TestSuite(object):\n+\n+    def __init__(self, path, output):\n+        self.path = path\n+        self.output = output\n+\n+    def should_generate(self, path):\n+        if not os.path.isfile(path):\n+            return True\n+\n+        if any(module.modified for module in self.modules.values()):\n+            return True\n+\n+        return False\n+\n+    def find_modules(self):\n+        modules = []\n+        for root, _, files in os.walk(self.path):\n+            module_root = root[len(self.path):]\n+            module_root = [c for c in module_root.split(os.sep) if c]\n+\n+            tests_in_module = fnmatch.filter(files, \"*.c\")\n+\n+            for test_file in tests_in_module:\n+                full_path = os.path.join(root, test_file)\n+                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n+\n+                modules.append((full_path, module_name))\n+\n+        return modules\n+\n+    def load_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        cache = {}\n+\n+        try:\n+            fp = open(path, 'rb')\n+            cache = pickle.load(fp)\n+            fp.close()\n+        except (IOError, ValueError):\n+            pass\n+\n+        return cache\n+\n+    def save_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        with open(path, 'wb') as cache:\n+            pickle.dump(self.modules, cache)\n+\n+    def load(self, force = False):\n+        module_data = self.find_modules()\n+        self.modules = {} if force else self.load_cache()\n+\n+        for path, name in module_data:\n+            if name not in self.modules:\n+                self.modules[name] = Module(name)\n+\n+            if not self.modules[name].refresh(path):\n+                del self.modules[name]\n+\n+    def disable(self, excluded):\n+        for exclude in excluded:\n+            for module in self.modules.values():\n+                name = module.clean_name()\n+                if name.startswith(exclude):\n+                    module.enabled = False\n+                    module.modified = True\n+\n+    def suite_count(self):\n+        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n+\n+    def callback_count(self):\n+        return sum(len(module.callbacks) for module in self.modules.values())\n+\n+    def write(self):\n+        output = os.path.join(self.output, 'clar.suite')\n+\n+        if not self.should_generate(output):\n+            return False\n+\n+        with open(output, 'w') as data:\n+            modules = sorted(self.modules.values(), key=lambda module: module.name)\n+\n+            for module in modules:\n+                t = Module.DeclarationTemplate(module)\n+                data.write(t.render())\n+\n+            for module in modules:\n+                t = Module.CallbacksTemplate(module)\n+                data.write(t.render())\n+\n+            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n+                Module.InfoTemplate(module).render() for module in modules\n+            ) + \"\\n};\\n\"\n+\n+            data.write(suites)\n+\n+            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n+            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n+\n+        self.save_cache()\n+        return True\n+\n+if __name__ == '__main__':\n+    from optparse import OptionParser\n+\n+    parser = OptionParser()\n+    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n+    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n+    parser.add_option('-o', '--output', dest='output')\n+\n+    options, args = parser.parse_args()\n+    if len(args) > 1:\n+        print(\"More than one path given\")\n+        sys.exit(1)\n+\n+    path = args.pop() if args else '.'\n+    output = options.output or path\n+    suite = TestSuite(path, output)\n+    suite.load(options.force)\n+    suite.disable(options.excluded)\n+    if suite.write():\n+        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n+\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nnew file mode 100644\nindex 0000000000..3e8ae0a3a5\n--- /dev/null\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -0,0 +1,5 @@\n+clar.suite\n+.clarcache\n+clar_test\n+*.o\n+\ndiff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\nnew file mode 100644\nindex 0000000000..93c6b2ad32\n--- /dev/null\n+++ b/t/unit-tests/clar/test/Makefile\n@@ -0,0 +1,39 @@\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+#\n+# Set up the path to the clar sources and to the fixtures directory\n+#\n+# The fixture path needs to be an absolute path so it can be used\n+# even after we have chdir'ed into the test directory while testing.\n+#\n+CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n+TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n+CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n+CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n+\n+CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n+\n+.PHONY: clean\n+\n+# list the objects that go into our test\n+objects = main.o sample.o\n+\n+# build the test executable itself\n+clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n+\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n+\n+# test object files depend on clar macros\n+$(objects) : $(CLAR_PATH)clar.h\n+\n+# build the clar.suite file of test metadata\n+clar.suite:\n+\tpython \"$(CLAR_PATH)generate.py\" .\n+\n+# remove all generated files\n+clean:\n+\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\ndiff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\nnew file mode 100644\nindex 0000000000..0fcaa639aa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/clar_test.h\n@@ -0,0 +1,16 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST__\n+#define __CLAR_TEST__\n+\n+/* Import the standard clar helper functions */\n+#include \"clar.h\"\n+\n+/* Your custom shared includes / defines here */\n+extern int global_test_counter;\n+\n+#endif\ndiff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\nnew file mode 100644\nindex 0000000000..59e56ad255\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c\n@@ -0,0 +1,40 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Sample main() for clar tests.\n+ *\n+ * You should write your own main routine for clar tests that does specific\n+ * setup and teardown as necessary for your application.  The only required\n+ * line is the call to `clar_test(argc, argv)`, which will execute the test\n+ * suite.  If you want to check the return value of the test application,\n+ * your main() should return the same value returned by clar_test().\n+ */\n+\n+int global_test_counter = 0;\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\tint ret;\n+\n+\t/* Your custom initialization here */\n+\tglobal_test_counter = 0;\n+\n+\t/* Run the test suite */\n+\tret = clar_test(argc, argv);\n+\n+\t/* Your custom cleanup here */\n+\tcl_assert_equal_i(8, global_test_counter);\n+\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\nnew file mode 100644\nindex 0000000000..a4d91b72fa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c.sample\n@@ -0,0 +1,27 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Minimal main() for clar tests.\n+ *\n+ * Modify this with any application specific setup or teardown that you need.\n+ * The only required line is the call to `clar_test(argc, argv)`, which will\n+ * execute the test suite.  If you want to check the return value of the test\n+ * application, main() should return the same value returned by clar_test().\n+ */\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\t/* Run the test suite */\n+\treturn clar_test(argc, argv);\n+}\ndiff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\nnew file mode 100644\nindex 0000000000..220f4aa98a\n--- /dev/null\n+++ b/t/unit-tests/clar/test/resources/test/file\n@@ -0,0 +1 @@\n+File\ndiff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\nnew file mode 100644\nindex 0000000000..faa1209262\n--- /dev/null\n+++ b/t/unit-tests/clar/test/sample.c\n@@ -0,0 +1,84 @@\n+#include \"clar_test.h\"\n+#include <sys/stat.h>\n+\n+static int file_size(const char *filename)\n+{\n+\tstruct stat st;\n+\n+\tif (stat(filename, &st) == 0)\n+\t\treturn (int)st.st_size;\n+\treturn -1;\n+}\n+\n+void test_sample__initialize(void)\n+{\n+\tglobal_test_counter++;\n+}\n+\n+void test_sample__cleanup(void)\n+{\n+\tcl_fixture_cleanup(\"test\");\n+\n+\tcl_assert(file_size(\"test/file\") == -1);\n+}\n+\n+void test_sample__1(void)\n+{\n+\tcl_assert(1);\n+\tcl_must_pass(0);  /* 0 == success */\n+\tcl_must_fail(-1); /* <0 == failure */\n+\tcl_must_pass(-1); /* demonstrate a failing call */\n+}\n+\n+void test_sample__2(void)\n+{\n+\tcl_fixture_sandbox(\"test\");\n+\n+\tcl_assert(file_size(\"test/nonexistent\") == -1);\n+\tcl_assert(file_size(\"test/file\") > 0);\n+\tcl_assert(100 == 101);\n+}\n+\n+void test_sample__strings(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_s(\"expected\", actual);\n+\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n+\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n+}\n+\n+void test_sample__strings_with_length(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_strn(\"expected_\", actual, 8);\n+\tcl_assert_equal_strn(\"exactly\", actual, 2);\n+\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n+\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n+}\n+\n+void test_sample__int(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i(100, value);\n+\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n+}\n+\n+void test_sample__int_fmt(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n+}\n+\n+void test_sample__bool(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n+\tcl_assert_equal_b(0, value);\n+}\n+\n+void test_sample__ptr(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n+\tcl_assert_equal_p(&actual, actual);\n+}\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"500984","messageId":"086dd728a72bb418e0d1b616d4f0318282b9f24d.1723712012.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723712012.git.ps@pks.im","subject":"[PATCH v4 1/7] t: do not pass GIT_TEST_OPTS to unit tests with prove","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-15T09:47:36Z","receivedAt":"2024-08-15T11:22:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When using the prove target, we append GIT_TEST_OPTS to the arguments\nthat we execute each of the tests with. This doesn't only include the\nintended test scripts, but also ends up passing the arguments to our\nunit tests. This is unintentional though as they do not even know to\ninterpret those arguments, and is inconsistent with how we execute unit\ntests without prove.\n\nThis isn't much of an issue because our current set of unit tests mostly\nignore their arguments anyway. With the introduction of clar-based unit\ntests this is about to become an issue though, as these do parse their\ncommand line argument to alter behaviour.\n\nPrepare for this by passing GIT_TEST_OPTS to \"run-test.sh\" via an\nenvironment variable. Like this, we can conditionally forward it to our\ntest scripts, only.\n---\n t/Makefile    | 3 ++-\n t/run-test.sh | 2 +-\n 2 files changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/Makefile b/t/Makefile\nindex 4c30e7c06f..d2212de0b7 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -68,7 +68,8 @@ failed:\n \ttest -z \"$$failed\" || $(MAKE) $$failed\n \n prove: pre-clean check-chainlint $(TEST_LINT)\n-\t@echo \"*** prove (shell & unit tests) ***\"; $(CHAINLINTSUPPRESS) TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS) :: $(GIT_TEST_OPTS)\n+\t@echo \"*** prove (shell & unit tests) ***\"\n+\t@$(CHAINLINTSUPPRESS) TEST_OPTIONS='$(GIT_TEST_OPTS)' TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS)\n \t$(MAKE) clean-except-prove-cache\n \n $(T):\ndiff --git a/t/run-test.sh b/t/run-test.sh\nindex 13c353b91b..63328ac630 100755\n--- a/t/run-test.sh\n+++ b/t/run-test.sh\n@@ -10,7 +10,7 @@ case \"$1\" in\n \t\techo >&2 \"ERROR: TEST_SHELL_PATH is empty or not set\"\n \t\texit 1\n \tfi\n-\texec \"${TEST_SHELL_PATH}\" \"$@\"\n+\texec \"${TEST_SHELL_PATH}\" \"$@\" ${TEST_OPTIONS}\n \t;;\n *)\n \texec \"$@\"\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"500991","messageId":"e0f99874cc0233338d225cf98e88b7e9b51a55f5.1723712012.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723712012.git.ps@pks.im","subject":"[PATCH v4 3/7] t/clar: fix whitespace errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-15T09:47:42Z","receivedAt":"2024-08-15T11:22:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Fix whitespace errors in the clar that make git-apply(1) unhappy. This\nhas been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/97\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/fs.h       | 2 +-\n t/unit-tests/clar/clar/sandbox.h  | 5 ++---\n t/unit-tests/clar/generate.py     | 1 -\n t/unit-tests/clar/test/.gitignore | 1 -\n 4 files changed, 3 insertions(+), 6 deletions(-)\n\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nindex a6eda5e5dc..3e39890bd3 100644\n--- a/t/unit-tests/clar/clar/fs.h\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -146,7 +146,7 @@ fs_rm_wait(WCHAR *_wpath)\n \t\t\tERROR_PATH_NOT_FOUND == last_error)\n \t\t\treturn 0;\n \n-\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n \t}\n \twhile (retries++ <= RM_RETRY_COUNT);\n \ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 0ba1479620..7c177f3525 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -25,9 +25,9 @@ find_tmp_path(char *buffer, size_t length)\n \tstatic const size_t var_count = 5;\n \tstatic const char *env_vars[] = {\n \t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n- \t};\n+\t};\n \n- \tsize_t i;\n+\tsize_t i;\n \n \tfor (i = 0; i < var_count; ++i) {\n \t\tconst char *env = getenv(env_vars[i]);\n@@ -151,4 +151,3 @@ const char *clar_sandbox_path(void)\n {\n \treturn _clar_path;\n }\n-\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nindex 931b4d60d9..80996ac3e7 100755\n--- a/t/unit-tests/clar/generate.py\n+++ b/t/unit-tests/clar/generate.py\n@@ -264,4 +264,3 @@ def write(self):\n     suite.disable(options.excluded)\n     if suite.write():\n         print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n-\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nindex 3e8ae0a3a5..a477d0c40c 100644\n--- a/t/unit-tests/clar/test/.gitignore\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -2,4 +2,3 @@ clar.suite\n .clarcache\n clar_test\n *.o\n-\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"500992","messageId":"5b8a64ae792c96be87ef65b26413ad5d150e0f0d.1723712012.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723712012.git.ps@pks.im","subject":"[PATCH v4 5/7] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-15T09:47:50Z","receivedAt":"2024-08-15T11:22:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Wire up the clar unit testing framework by introducing a new\n\"unit-tests\" executable. In contrast to the existing framework, this\nwill result in a single executable for all test suites. The ability to\npick specific tests to execute is retained via functionality built into\nthe clar itself.\n\nNote that we need to be a bit careful about how we need to invalidate\nour Makefile rules. While we obviously have to regenerate the clar suite\nwhen our test suites change, we also have to invalidate it in case any\nof the test suites gets removed. We do so by using our typical pattern\nof creating a `GIT-TEST-SUITES` file that gets updated whenever the set\nof test suites changes, so that we can easily depend on that file.\n\nAnother specialty is that we generate a \"clar-decls.h\" file. The test\nfunctions are neither static, nor do they have external declarations.\nThis is because they are getting parsed via \"generate.py\", which then\ncreates the external generations that get populated into an array. These\ndeclarations are only seen by the main function though.\n\nThe consequence is that we will get a bunch of \"missing prototypes\"\nerrors from our compiler for each of these test functions. To fix those\nerrors, we extract the `extern` declarations from \"clar.suite\" and put\nthem into a standalone header that then gets included by each of our\nunit tests. This gets rid of compiler warnings for every function which\nhas been extracted by \"generate.py\". More importantly though, it does\n_not_ get rid of warnings in case a function really isn't being used by\nanything. Thus, it would cause a compiler error if a function name was\nmistyped and thus not picked up by \"generate.py\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Makefile                       | 35 +++++++++++++++++++++---\n t/Makefile                     |  1 +\n t/unit-tests/.gitignore        |  2 ++\n t/unit-tests/clar-generate.awk | 50 ++++++++++++++++++++++++++++++++++\n t/unit-tests/unit-test.c       | 18 ++++++++++++\n t/unit-tests/unit-test.h       |  3 ++\n 7 files changed, 106 insertions(+), 4 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\ndiff --git a/.gitignore b/.gitignore\nindex 8caf3700c2..6687bd6db4 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,6 +9,7 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n+/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex 674b0ac4e1..1311bbbbed 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1334,6 +1334,11 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n+UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n+\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n@@ -2713,6 +2718,7 @@ OBJECTS += $(XDIFF_OBJS)\n OBJECTS += $(FUZZ_OBJS)\n OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n OBJECTS += $(UNIT_TEST_OBJS)\n+OBJECTS += $(UNIT_TESTS_OBJS)\n \n ifndef NO_CURL\n \tOBJECTS += http.o http-walker.o remote-curl.o\n@@ -3215,7 +3221,7 @@ endif\n \n test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))\n \n-all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)\n+all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n \n bin-wrappers/%: wrap-for-bin.sh\n \t$(call mkdir_p_parent_template)\n@@ -3279,6 +3285,7 @@ CHK_HDRS = $(filter-out $(EXCEPT_HDRS),$(LIB_H))\n HCO = $(patsubst %.h,%.hco,$(CHK_HDRS))\n HCC = $(HCO:hco=hcc)\n \n+$(UNIT_TEST_DIR)/unit-test.hcc: $(UNIT_TEST_DIR)/unit-test.h $(UNIT_TEST_DIR)/clar-decls.h\n %.hcc: %.h\n \t@echo '#include \"git-compat-util.h\"' >$@\n \t@echo '#include \"$<\"' >>$@\n@@ -3646,7 +3653,7 @@ endif\n \n artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n \t\tGIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \\\n-\t\t$(UNIT_TEST_PROGS) $(MOFILES)\n+\t\t$(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) $(MOFILES)\n \t$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \\\n \t\tSHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'\n \ttest -n \"$(ARTIFACTS_DIRECTORY)\"\n@@ -3702,6 +3709,7 @@ cocciclean:\n \n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n+\t$(RM) GIT-TEST-SUITES $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/clar-decls.h\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3861,7 +3869,26 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n \n+GIT-TEST-SUITES: FORCE\n+\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n+\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n+\t\techo >&2 \"    * new test suites\"; \\\n+\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n+            fi\n+\n+$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n+\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n+\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n+\tdone >$@\n+$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n+\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n+$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR)\n+$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n+\t$(call mkdir_p_parent_template)\n+\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n+\n .PHONY: build-unit-tests unit-tests\n-build-unit-tests: $(UNIT_TEST_PROGS)\n-unit-tests: $(UNIT_TEST_PROGS) t/helper/test-tool$X\n+build-unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n+unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) t/helper/test-tool$X\n \t$(MAKE) -C t/ unit-tests\ndiff --git a/t/Makefile b/t/Makefile\nindex d2212de0b7..131ffd778f 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -48,6 +48,7 @@ CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.tes\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n+UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n UNIT_TESTS = $(sort $(UNIT_TEST_PROGRAMS))\n UNIT_TESTS_NO_DIR = $(notdir $(UNIT_TESTS))\n \ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex 5e56e040ec..d0632ec7f9 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1 +1,3 @@\n /bin\n+/clar.suite\n+/clar-decls.h\ndiff --git a/t/unit-tests/clar-generate.awk b/t/unit-tests/clar-generate.awk\nnew file mode 100644\nindex 0000000000..ab71ce6c9f\n--- /dev/null\n+++ b/t/unit-tests/clar-generate.awk\n@@ -0,0 +1,50 @@\n+function add_suite(suite, initialize, cleanup, count) {\n+       if (!suite) return\n+       suite_count++\n+       callback_count += count\n+       suites = suites \"    {\\n\"\n+       suites = suites \"        \\\"\" suite \"\\\",\\n\"\n+       suites = suites \"        \" initialize \",\\n\"\n+       suites = suites \"        \" cleanup \",\\n\"\n+       suites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n+       suites = suites \"    },\\n\"\n+}\n+\n+BEGIN {\n+       suites = \"static struct clar_suite _clar_suites[] = {\\n\"\n+}\n+\n+{\n+       print\n+       name = $3; sub(/\\(.*$/, \"\", name)\n+       suite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n+       short_name = name; sub(/^.*__/, \"\", short_name)\n+       cb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n+       if (suite != prev_suite) {\n+               add_suite(prev_suite, initialize, cleanup, count)\n+               if (callbacks) callbacks = callbacks \"};\\n\"\n+               callbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n+               initialize = \"{ NULL, NULL }\"\n+               cleanup = \"{ NULL, NULL }\"\n+               count = 0\n+               prev_suite = suite\n+       }\n+       if (short_name == \"initialize\") {\n+               initialize = cb\n+       } else if (short_name == \"cleanup\") {\n+               cleanup = cb\n+       } else {\n+               callbacks = callbacks \"    \" cb \",\\n\"\n+               count++\n+       }\n+}\n+\n+END {\n+       add_suite(suite, initialize, cleanup, count)\n+       suites = suites \"};\"\n+       if (callbacks) callbacks = callbacks \"};\"\n+       print callbacks\n+       print suites\n+       print \"static const size_t _clar_suite_count = \" suite_count \";\"\n+       print \"static const size_t _clar_callback_count = \" callback_count \";\"\n+}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nnew file mode 100644\nindex 0000000000..32a81299e9\n--- /dev/null\n+++ b/t/unit-tests/unit-test.c\n@@ -0,0 +1,18 @@\n+#include \"unit-test.h\"\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tconst char **argv_copy;\n+\tint ret;\n+\n+\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n+\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tCOPY_ARRAY(argv_copy, argv, argc);\n+\targv_copy[argc++] = \"-t\";\n+\targv_copy[argc] = NULL;\n+\n+\tret = clar_test(argc, (char **) argv_copy);\n+\n+\tfree(argv_copy);\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nnew file mode 100644\nindex 0000000000..66ec2387cc\n--- /dev/null\n+++ b/t/unit-tests/unit-test.h\n@@ -0,0 +1,3 @@\n+#include \"git-compat-util.h\"\n+#include \"clar/clar.h\"\n+#include \"clar-decls.h\"\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501018","messageId":"xmqqy14xu58v.fsf@gitster.g","threadId":"61874","inReplyTo":"cover.1723712012.git.ps@pks.im","subject":"Re: [PATCH v4 0/7] Introduce clar testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-15T16:21:04Z","receivedAt":"2024-08-15T16:21:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hi,\n>\n> this is the fourth version of my patch series that introduces the clar\n> testing framework for our unit tests.\n>\n> Changes compared to v3:\n>\n>   - Seeing that the overall feedback seems to be rather positive, I've\n>     now dropped the \"RFC\" prefix.\n>\n>   - Fix another whitespace-damaged line in the clar unit test. I've\n>     adapted the upstream pull request accordingly.\n>\n>   - Fix `make hdr-check`, which failed due to the missing dependency on\n>     \"clar-decls.h\".\n\nSimilarly, lack of clar.suite still makes \"make sparse\" fail, it\nseems.\n\n$ make NO_REGEX=NoThanks -j32 sparse\n    ...\n    SP builtin/help.c\n    CC t/unit-tests/ctype.o\n    CC t/unit-tests/strvec.o\n    CC t/unit-tests/clar/clar.o\n    CC t/unit-tests/unit-test.o\nt/unit-tests/clar/clar.c:209:10: fatal error: clar.suite: No such file or directory\n  209 | #include \"clar.suite\"\n      |          ^~~~~~~~~~~~\ncompilation terminated.\nmake: *** [Makefile:2762: t/unit-tests/clar/clar.o] Error 1\nmake: *** Waiting for unfinished jobs....\n\nThe NO_REGEX is there to force us to go check the compat/regex.h as well.\n\n>     +@@ Makefile: CHK_HDRS = $(filter-out $(EXCEPT_HDRS),$(LIB_H))\n>     + HCO = $(patsubst %.h,%.hco,$(CHK_HDRS))\n>     + HCC = $(HCO:hco=hcc)\n>     + \n>     ++$(UNIT_TEST_DIR)/unit-test.hcc: $(UNIT_TEST_DIR)/unit-test.h $(UNIT_TEST_DIR)/clar-decls.h\n>     + %.hcc: %.h\n>     + \t@echo '#include \"git-compat-util.h\"' >$@\n>     + \t@echo '#include \"$<\"' >>$@\n\nThis is a nice addition.\n\nThanks.\n\n"},{"id":"501084","messageId":"Zr7fYYUWhkfT9_-8@tanuki","threadId":"61874","inReplyTo":"xmqqy14xu58v.fsf@gitster.g","subject":"Re: [PATCH v4 0/7] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T05:10:57Z","receivedAt":"2024-08-16T05:11:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Aug 15, 2024 at 09:21:04AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Hi,\n> >\n> > this is the fourth version of my patch series that introduces the clar\n> > testing framework for our unit tests.\n> >\n> > Changes compared to v3:\n> >\n> >   - Seeing that the overall feedback seems to be rather positive, I've\n> >     now dropped the \"RFC\" prefix.\n> >\n> >   - Fix another whitespace-damaged line in the clar unit test. I've\n> >     adapted the upstream pull request accordingly.\n> >\n> >   - Fix `make hdr-check`, which failed due to the missing dependency on\n> >     \"clar-decls.h\".\n> \n> Similarly, lack of clar.suite still makes \"make sparse\" fail, it\n> seems.\n> \n> $ make NO_REGEX=NoThanks -j32 sparse\n>     ...\n>     SP builtin/help.c\n>     CC t/unit-tests/ctype.o\n>     CC t/unit-tests/strvec.o\n>     CC t/unit-tests/clar/clar.o\n>     CC t/unit-tests/unit-test.o\n> t/unit-tests/clar/clar.c:209:10: fatal error: clar.suite: No such file or directory\n>   209 | #include \"clar.suite\"\n>       |          ^~~~~~~~~~~~\n> compilation terminated.\n> make: *** [Makefile:2762: t/unit-tests/clar/clar.o] Error 1\n> make: *** Waiting for unfinished jobs....\n> \n> The NO_REGEX is there to force us to go check the compat/regex.h as well.\n\nHuh, OK. Wonder why the CI didn't catch this. I'll have another look,\nthanks.\n\nPatrick\n"},{"id":"501092","messageId":"cover.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[PATCH v5 0/9] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:34Z","receivedAt":"2024-08-16T07:36:12Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the fifth version of my patch series that introduces the clar\ntesting framework for our unit tests.\n\nChanges compared to v4:\n\n  - The whitespace fixes have been merged upstream, so I've updated the\n    embedded copy of clar and dropped the subsequent patch that fixed\n    them in our copy. The NonStop compatibility fixes have not yet been\n    merged as the pull request needs some more work.\n\n  - Both \"clar-decls.h\" and \"clar.suite\" are now part of GENERATED_H.\n    This brings removal of these files via \"make clean\" for free.\n\n  - The \"sparse\" target already depends on GENERATED_H, but in a broken\n    way. I've fixed that in a new commit.\n\n  - The \"sparse\" target no longer checks external sources, including the\n    clar sources.\n\n  - The \"hdr-check\" target now depends on GENERATED_H, as well. This\n    avoids having to manually wire up dependencies on generated headers\n    per file, which seems rather unmaintainable to me.\n\nWith this, the \"hdr-check\" and \"sparse\" targets all work on my machine\nnow.\n\nThanks!\n\nPatrick\n\nPatrick Steinhardt (9):\n  t: do not pass GIT_TEST_OPTS to unit tests with prove\n  t: import the clar unit testing framework\n  t/clar: fix compatibility with NonStop\n  Makefile: fix sparse dependency on GENERATED_H\n  Makefile: make hdr-check depend on generated headers\n  Makefile: do not use sparse on third-party sources\n  Makefile: wire up the clar unit testing framework\n  t/unit-tests: convert strvec tests to use clar\n  t/unit-tests: convert ctype tests to use clar\n\n .gitignore                                 |   1 +\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |  53 +-\n t/Makefile                                 |   4 +-\n t/run-test.sh                              |   2 +-\n t/unit-tests/.gitignore                    |   2 +\n t/unit-tests/clar-generate.awk             |  50 ++\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n t/unit-tests/{t-strvec.c => strvec.c}      | 119 ++-\n t/unit-tests/unit-test.c                   |  17 +\n t/unit-tests/unit-test.h                   |   3 +\n 29 files changed, 3166 insertions(+), 102 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\nRange-diff against v4:\n 1:  086dd728a7 !  1:  832dc0496f t: do not pass GIT_TEST_OPTS to unit tests with prove\n    @@ Commit message\n         environment variable. Like this, we can conditionally forward it to our\n         test scripts, only.\n     \n    +    Signed-off-by: Patrick Steinhardt <ps@pks.im>\n    +\n      ## t/Makefile ##\n     @@ t/Makefile: failed:\n      \ttest -z \"$$failed\" || $(MAKE) $$failed\n 2:  5c22e0b3b9 !  2:  3690607933 t: import the clar unit testing framework\n    @@ Metadata\n      ## Commit message ##\n         t: import the clar unit testing framework\n     \n    -    Import the clar unit testing framework at commit faa8419 (Merge pull\n    -    request #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\n    +    Import the clar unit testing framework at commit 1516124 (Merge pull\n    +    request #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\n         will be wired up in subsequent commits.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n    @@ t/unit-tests/clar/clar/fs.h (new)\n     +\t\t\tERROR_PATH_NOT_FOUND == last_error)\n     +\t\t\treturn 0;\n     +\n    -+\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n    ++\t\tSleep(RM_RETRY_DELAY * retries * retries);\n     +\t}\n     +\twhile (retries++ <= RM_RETRY_COUNT);\n     +\n    @@ t/unit-tests/clar/clar/sandbox.h (new)\n     +\tstatic const size_t var_count = 5;\n     +\tstatic const char *env_vars[] = {\n     +\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n    -+ \t};\n    ++\t};\n     +\n    -+ \tsize_t i;\n    ++\tsize_t i;\n     +\n     +\tfor (i = 0; i < var_count; ++i) {\n     +\t\tconst char *env = getenv(env_vars[i]);\n    @@ t/unit-tests/clar/clar/sandbox.h (new)\n     +{\n     +\treturn _clar_path;\n     +}\n    -+\n     \n      ## t/unit-tests/clar/clar/summary.h (new) ##\n     @@\n    @@ t/unit-tests/clar/generate.py (new)\n     +    suite.disable(options.excluded)\n     +    if suite.write():\n     +        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n    -+\n     \n      ## t/unit-tests/clar/test/.gitignore (new) ##\n     @@\n    @@ t/unit-tests/clar/test/.gitignore (new)\n     +.clarcache\n     +clar_test\n     +*.o\n    -+\n     \n      ## t/unit-tests/clar/test/Makefile (new) ##\n     @@\n 4:  75e097dfa4 =  3:  db53673294 t/clar: fix compatibility with NonStop\n 3:  e0f99874cc !  4:  b6199c88dd t/clar: fix whitespace errors\n    @@ Metadata\n     Author: Patrick Steinhardt <ps@pks.im>\n     \n      ## Commit message ##\n    -    t/clar: fix whitespace errors\n    -\n    -    Fix whitespace errors in the clar that make git-apply(1) unhappy. This\n    -    has been cherry-picked from the upstream pull request at [1].\n    -\n    -    [1]: https://github.com/clar-test/clar/pull/97\n    +    Makefile: fix sparse dependency on GENERATED_H\n    +\n    +    The \"check\" Makefile target is essentially an alias around the \"sparse\"\n    +    target. The one difference though is that it will tell users to instead\n    +    run the \"test\" target in case they do not have sparse(1) installed, as\n    +    chances are high that they wanted to execute the test suite rather than\n    +    doing semantic checks.\n    +\n    +    But even though the \"check\" target ultimately just ends up executing\n    +    `make sparse`, it still depends on our generated headers. This does not\n    +    make any sense though: they are irrelevant for the \"test\" target advice,\n    +    and if these headers are required for the \"sparse\" target they must be\n    +    declared as a dependency on the aliased target, not the alias.\n    +\n    +    But even moving the dependency to the \"sparse\" target is wrong, as\n    +    concurrent builds may then end up generating the headers and running\n    +    sparse concurrently. Instead, we make them a dependency of the specific\n    +    objects. While that is overly broad, it does ensure correct ordering.\n    +    The alternative, specifying which file depends on what generated header\n    +    explicitly, feels rather unmaintainable.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    - ## t/unit-tests/clar/clar/fs.h ##\n    -@@ t/unit-tests/clar/clar/fs.h: fs_rm_wait(WCHAR *_wpath)\n    - \t\t\tERROR_PATH_NOT_FOUND == last_error)\n    - \t\t\treturn 0;\n    - \n    --\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n    -+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n    - \t}\n    - \twhile (retries++ <= RM_RETRY_COUNT);\n    + ## Makefile ##\n    +@@ Makefile: check-sha1:: t/helper/test-tool$X\n      \n    -\n    - ## t/unit-tests/clar/clar/sandbox.h ##\n    -@@ t/unit-tests/clar/clar/sandbox.h: find_tmp_path(char *buffer, size_t length)\n    - \tstatic const size_t var_count = 5;\n    - \tstatic const char *env_vars[] = {\n    - \t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n    -- \t};\n    -+\t};\n    + SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n      \n    -- \tsize_t i;\n    -+\tsize_t i;\n    +-$(SP_OBJ): %.sp: %.c %.o\n    ++$(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n    + \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n    + \t\t-Wsparse-error \\\n    + \t\t$(SPARSE_FLAGS) $(SP_EXTRA_FLAGS) $< && \\\n    +@@ Makefile: style:\n    + \tgit clang-format --style file --diff --extensions c,h\n      \n    - \tfor (i = 0; i < var_count; ++i) {\n    - \t\tconst char *env = getenv(env_vars[i]);\n    -@@ t/unit-tests/clar/clar/sandbox.h: const char *clar_sandbox_path(void)\n    - {\n    - \treturn _clar_path;\n    - }\n    --\n    -\n    - ## t/unit-tests/clar/generate.py ##\n    -@@ t/unit-tests/clar/generate.py: def write(self):\n    -     suite.disable(options.excluded)\n    -     if suite.write():\n    -         print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n    --\n    -\n    - ## t/unit-tests/clar/test/.gitignore ##\n    -@@ t/unit-tests/clar/test/.gitignore: clar.suite\n    - .clarcache\n    - clar_test\n    - *.o\n    --\n    + .PHONY: check\n    +-check: $(GENERATED_H)\n    ++check:\n    + \t@if sparse; \\\n    + \tthen \\\n    + \t\techo >&2 \"Use 'make sparse' instead\"; \\\n -:  ---------- >  5:  06364b2b72 Makefile: make hdr-check depend on generated headers\n -:  ---------- >  6:  88ea94ce16 Makefile: do not use sparse on third-party sources\n 5:  5b8a64ae79 !  7:  05bcb5bef6 Makefile: wire up the clar unit testing framework\n    @@ .gitignore\n      /bin-wrappers/\n     \n      ## Makefile ##\n    +@@ Makefile: REFTABLE_TEST_LIB = reftable/libreftable_test.a\n    + GENERATED_H += command-list.h\n    + GENERATED_H += config-list.h\n    + GENERATED_H += hook-list.h\n    ++GENERATED_H += $(UNIT_TEST_DIR)/clar-decls.h\n    ++GENERATED_H += $(UNIT_TEST_DIR)/clar.suite\n    + \n    + .PHONY: generated-hdrs\n    + generated-hdrs: $(GENERATED_H)\n     @@ Makefile: THIRD_PARTY_SOURCES += sha1dc/%\n      THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n      THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n    @@ Makefile: endif\n      \n      bin-wrappers/%: wrap-for-bin.sh\n      \t$(call mkdir_p_parent_template)\n    -@@ Makefile: CHK_HDRS = $(filter-out $(EXCEPT_HDRS),$(LIB_H))\n    - HCO = $(patsubst %.h,%.hco,$(CHK_HDRS))\n    - HCC = $(HCO:hco=hcc)\n    - \n    -+$(UNIT_TEST_DIR)/unit-test.hcc: $(UNIT_TEST_DIR)/unit-test.h $(UNIT_TEST_DIR)/clar-decls.h\n    - %.hcc: %.h\n    - \t@echo '#include \"git-compat-util.h\"' >$@\n    - \t@echo '#include \"$<\"' >>$@\n     @@ Makefile: endif\n      \n      artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n    @@ Makefile: cocciclean:\n      \n      clean: profile-clean coverage-clean cocciclean\n      \t$(RM) -r .build $(UNIT_TEST_BIN)\n    -+\t$(RM) GIT-TEST-SUITES $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/clar-decls.h\n    ++\t$(RM) GIT-TEST-SUITES\n      \t$(RM) po/git.pot po/git-core.pot\n      \t$(RM) git.res\n      \t$(RM) $(OBJECTS)\n 6:  bc4e23d666 =  8:  8f56b4d626 t/unit-tests: convert strvec tests to use clar\n 7:  0a7fe8775a =  9:  ca09d19fd5 t/unit-tests: convert ctype tests to use clar\n\nbase-commit: 406f326d271e0bacecdb00425422c5fa3f314930\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501087","messageId":"06364b2b72202868c8fec804fd91264316e1595e.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 5/9] Makefile: make hdr-check depend on generated headers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:48Z","receivedAt":"2024-08-16T07:36:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"hdr-check\" Makefile target compiles each of our headers as a\nstandalone code unit to ensure that they are not missing any type\ndeclarations and can be included standalone.\n\nWith the next commit we will wire up the clar unit testing framework,\nwhich will have the effect that some headers start depending on\ngenerated ones. While we could declare that dependency explicitly, it\ndoes not really feel very maintainable in the future.\n\nInstead, we do the same as in the preceding commit and have the objects\ndepend on all of our generated headers. While again overly broad, it is\neasy to maintain and generating headers is not an expensive thing to do\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 0736d3c88e..5384bf8104 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3283,7 +3283,7 @@ HCC = $(HCO:hco=hcc)\n \t@echo '#include \"git-compat-util.h\"' >$@\n \t@echo '#include \"$<\"' >>$@\n \n-$(HCO): %.hco: %.hcc FORCE\n+$(HCO): %.hco: %.hcc $(GENERATED_H) FORCE\n \t$(QUIET_HDR)$(CC) $(ALL_CFLAGS) -o /dev/null -c -xc $<\n \n .PHONY: hdr-check $(HCO)\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501088","messageId":"b6199c88dd7f07d63ea1a9bed730c875b3748814.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 4/9] Makefile: fix sparse dependency on GENERATED_H","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:45Z","receivedAt":"2024-08-16T07:36:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"check\" Makefile target is essentially an alias around the \"sparse\"\ntarget. The one difference though is that it will tell users to instead\nrun the \"test\" target in case they do not have sparse(1) installed, as\nchances are high that they wanted to execute the test suite rather than\ndoing semantic checks.\n\nBut even though the \"check\" target ultimately just ends up executing\n`make sparse`, it still depends on our generated headers. This does not\nmake any sense though: they are irrelevant for the \"test\" target advice,\nand if these headers are required for the \"sparse\" target they must be\ndeclared as a dependency on the aliased target, not the alias.\n\nBut even moving the dependency to the \"sparse\" target is wrong, as\nconcurrent builds may then end up generating the headers and running\nsparse concurrently. Instead, we make them a dependency of the specific\nobjects. While that is overly broad, it does ensure correct ordering.\nThe alternative, specifying which file depends on what generated header\nexplicitly, feels rather unmaintainable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 674b0ac4e1..0736d3c88e 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3253,7 +3253,7 @@ check-sha1:: t/helper/test-tool$X\n \n SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n \n-$(SP_OBJ): %.sp: %.c %.o\n+$(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n \t\t-Wsparse-error \\\n \t\t$(SPARSE_FLAGS) $(SP_EXTRA_FLAGS) $< && \\\n@@ -3294,7 +3294,7 @@ style:\n \tgit clang-format --style file --diff --extensions c,h\n \n .PHONY: check\n-check: $(GENERATED_H)\n+check:\n \t@if sparse; \\\n \tthen \\\n \t\techo >&2 \"Use 'make sparse' instead\"; \\\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501089","messageId":"832dc0496fb4daaf858a4b44fba86695fb1e20cb.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 1/9] t: do not pass GIT_TEST_OPTS to unit tests with prove","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:37Z","receivedAt":"2024-08-16T07:36:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When using the prove target, we append GIT_TEST_OPTS to the arguments\nthat we execute each of the tests with. This doesn't only include the\nintended test scripts, but also ends up passing the arguments to our\nunit tests. This is unintentional though as they do not even know to\ninterpret those arguments, and is inconsistent with how we execute unit\ntests without prove.\n\nThis isn't much of an issue because our current set of unit tests mostly\nignore their arguments anyway. With the introduction of clar-based unit\ntests this is about to become an issue though, as these do parse their\ncommand line argument to alter behaviour.\n\nPrepare for this by passing GIT_TEST_OPTS to \"run-test.sh\" via an\nenvironment variable. Like this, we can conditionally forward it to our\ntest scripts, only.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/Makefile    | 3 ++-\n t/run-test.sh | 2 +-\n 2 files changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/Makefile b/t/Makefile\nindex 4c30e7c06f..d2212de0b7 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -68,7 +68,8 @@ failed:\n \ttest -z \"$$failed\" || $(MAKE) $$failed\n \n prove: pre-clean check-chainlint $(TEST_LINT)\n-\t@echo \"*** prove (shell & unit tests) ***\"; $(CHAINLINTSUPPRESS) TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS) :: $(GIT_TEST_OPTS)\n+\t@echo \"*** prove (shell & unit tests) ***\"\n+\t@$(CHAINLINTSUPPRESS) TEST_OPTIONS='$(GIT_TEST_OPTS)' TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS)\n \t$(MAKE) clean-except-prove-cache\n \n $(T):\ndiff --git a/t/run-test.sh b/t/run-test.sh\nindex 13c353b91b..63328ac630 100755\n--- a/t/run-test.sh\n+++ b/t/run-test.sh\n@@ -10,7 +10,7 @@ case \"$1\" in\n \t\techo >&2 \"ERROR: TEST_SHELL_PATH is empty or not set\"\n \t\texit 1\n \tfi\n-\texec \"${TEST_SHELL_PATH}\" \"$@\"\n+\texec \"${TEST_SHELL_PATH}\" \"$@\" ${TEST_OPTIONS}\n \t;;\n *)\n \texec \"$@\"\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501090","messageId":"8f56b4d6264b8e5a85ba73bc8ac541efa066c8a6.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 8/9] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:58Z","receivedAt":"2024-08-16T07:36:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the strvec tests to use the new clar unit testing framework.\nThis is a first test balloon that demonstrates how the testing infra for\nclar-based tests looks like.\n\nThe tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\nrunning that binary, it generates TAP output:\n\n    # ./t/unit-tests/bin/unit-tests\n    TAP version 13\n    # start of suite 1: strvec\n    ok 1 - strvec::init\n    ok 2 - strvec::dynamic_init\n    ok 3 - strvec::clear\n    ok 4 - strvec::push\n    ok 5 - strvec::pushft_pushf\n    ok 6 - strvec::pushl\n    ok 7 - strvec::pushv\n    ok 8 - strvec::replace_at_head\n    ok 9 - strvec::replace_at_tail\n    ok 10 - strvec::replace_in_between\n    ok 11 - strvec::replace_with_substring\n    ok 12 - strvec::remove_at_head\n    ok 13 - strvec::remove_at_tail\n    ok 14 - strvec::remove_in_between\n    ok 15 - strvec::pop_empty_array\n    ok 16 - strvec::pop_non_empty_array\n    ok 17 - strvec::split_empty_string\n    ok 18 - strvec::split_single_item\n    ok 19 - strvec::split_multiple_items\n    ok 20 - strvec::split_whitespace_only\n    ok 21 - strvec::split_multiple_consecutive_whitespaces\n    ok 22 - strvec::detach\n    1..22\n\nThe binary also supports some parameters that allow us to run only a\nsubset of unit tests or alter the output:\n\n    $ ./t/unit-tests/bin/unit-tests -h\n    Usage: ./t/unit-tests/bin/unit-tests [options]\n\n    Options:\n      -sname        Run only the suite with `name` (can go to individual test name)\n      -iname        Include the suite with `name`\n      -xname        Exclude the suite with `name`\n      -v            Increase verbosity (show suite names)\n      -q            Only report tests that had an error\n      -Q            Quit as soon as a test fails\n      -t            Display results in tap format\n      -l            Print suite names\n      -r[filename]  Write summary file (to the optional filename)\n\nFurthermore, running `make unit-tests` runs the binary along with all\nthe other unit tests we have.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                              |   2 +-\n t/unit-tests/{t-strvec.c => strvec.c} | 119 ++++++++++----------------\n t/unit-tests/unit-test.c              |   3 +-\n 3 files changed, 45 insertions(+), 79 deletions(-)\n rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n\ndiff --git a/Makefile b/Makefile\nindex e6206ca8c7..b0c9a79c55 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n@@ -1353,7 +1354,6 @@ UNIT_TEST_PROGRAMS += t-reftable-merged\n UNIT_TEST_PROGRAMS += t-reftable-record\n UNIT_TEST_PROGRAMS += t-strbuf\n UNIT_TEST_PROGRAMS += t-strcmp-offset\n-UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-trailer\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/strvec.c\nsimilarity index 54%\nrename from t/unit-tests/t-strvec.c\nrename to t/unit-tests/strvec.c\nindex fa1a041469..d11ed0f28d 100644\n--- a/t/unit-tests/t-strvec.c\n+++ b/t/unit-tests/strvec.c\n@@ -1,52 +1,46 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n \n #define check_strvec(vec, ...) \\\n \tdo { \\\n \t\tconst char *expect[] = { __VA_ARGS__ }; \\\n-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n-\t\t\t\t\t\t (uintmax_t)i); \\\n-\t\t\t\t\tbreak; \\\n-\t\t\t\t} \\\n-\t\t\t} \\\n-\t\t} \\\n+\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n+\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n+\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n+\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n+\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n \t} while (0)\n \n-static void t_static_init(void)\n+void test_strvec__init(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_dynamic_init(void)\n+void test_strvec__dynamic_init(void)\n {\n \tstruct strvec vec;\n \tstrvec_init(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_clear(void)\n+void test_strvec__clear(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_push(&vec, \"foo\");\n \tstrvec_clear(&vec);\n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n }\n \n-static void t_push(void)\n+void test_strvec__push(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \n@@ -59,7 +53,7 @@ static void t_push(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushf(void)\n+void test_strvec__pushf(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushf(&vec, \"foo: %d\", 1);\n@@ -67,7 +61,7 @@ static void t_pushf(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushl(void)\n+void test_strvec__pushl(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -75,7 +69,7 @@ static void t_pushl(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pushv(void)\n+void test_strvec__pushv(void)\n {\n \tconst char *strings[] = {\n \t\t\"foo\", \"bar\", \"baz\", NULL,\n@@ -88,7 +82,7 @@ static void t_pushv(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_head(void)\n+void test_strvec__replace_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -97,7 +91,7 @@ static void t_replace_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_at_tail(void)\n+void test_strvec__replace_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -106,7 +100,7 @@ static void t_replace_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_in_between(void)\n+void test_strvec__replace_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -115,7 +109,7 @@ static void t_replace_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_replace_with_substring(void)\n+void test_strvec__replace_with_substring(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", NULL);\n@@ -124,7 +118,7 @@ static void t_replace_with_substring(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_head(void)\n+void test_strvec__remove_at_head(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -133,7 +127,7 @@ static void t_remove_at_head(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_at_tail(void)\n+void test_strvec__remove_at_tail(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -142,7 +136,7 @@ static void t_remove_at_tail(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_remove_in_between(void)\n+void test_strvec__remove_in_between(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -151,7 +145,7 @@ static void t_remove_in_between(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_empty_array(void)\n+void test_strvec__pop_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pop(&vec);\n@@ -159,7 +153,7 @@ static void t_pop_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_pop_non_empty_array(void)\n+void test_strvec__pop_non_empty_array(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n@@ -168,7 +162,7 @@ static void t_pop_non_empty_array(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_empty_string(void)\n+void test_strvec__split_empty_string(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"\");\n@@ -176,7 +170,7 @@ static void t_split_empty_string(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_single_item(void)\n+void test_strvec__split_single_item(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\");\n@@ -184,7 +178,7 @@ static void t_split_single_item(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_items(void)\n+void test_strvec__split_multiple_items(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo bar baz\");\n@@ -192,7 +186,7 @@ static void t_split_multiple_items(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_whitespace_only(void)\n+void test_strvec__split_whitespace_only(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \" \\t\\n\");\n@@ -200,7 +194,7 @@ static void t_split_whitespace_only(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_split_multiple_consecutive_whitespaces(void)\n+void test_strvec__split_multiple_consecutive_whitespaces(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tstrvec_split(&vec, \"foo\\n\\t bar\");\n@@ -208,7 +202,7 @@ static void t_split_multiple_consecutive_whitespaces(void)\n \tstrvec_clear(&vec);\n }\n \n-static void t_detach(void)\n+void test_strvec__detach(void)\n {\n \tstruct strvec vec = STRVEC_INIT;\n \tconst char **detached;\n@@ -216,40 +210,13 @@ static void t_detach(void)\n \tstrvec_push(&vec, \"foo\");\n \n \tdetached = strvec_detach(&vec);\n-\tcheck_str(detached[0], \"foo\");\n-\tcheck_pointer_eq(detached[1], NULL);\n+\tcl_assert_equal_s(detached[0], \"foo\");\n+\tcl_assert_equal_p(detached[1], NULL);\n \n-\tcheck_pointer_eq(vec.v, empty_strvec);\n-\tcheck_uint(vec.nr, ==, 0);\n-\tcheck_uint(vec.alloc, ==, 0);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n \n \tfree((char *) detached[0]);\n \tfree(detached);\n }\n-\n-int cmd_main(int argc, const char **argv)\n-{\n-\tTEST(t_static_init(), \"static initialization\");\n-\tTEST(t_dynamic_init(), \"dynamic initialization\");\n-\tTEST(t_clear(), \"clear\");\n-\tTEST(t_push(), \"push\");\n-\tTEST(t_pushf(), \"pushf\");\n-\tTEST(t_pushl(), \"pushl\");\n-\tTEST(t_pushv(), \"pushv\");\n-\tTEST(t_replace_at_head(), \"replace at head\");\n-\tTEST(t_replace_in_between(), \"replace in between\");\n-\tTEST(t_replace_at_tail(), \"replace at tail\");\n-\tTEST(t_replace_with_substring(), \"replace with substring\");\n-\tTEST(t_remove_at_head(), \"remove at head\");\n-\tTEST(t_remove_in_between(), \"remove in between\");\n-\tTEST(t_remove_at_tail(), \"remove at tail\");\n-\tTEST(t_pop_empty_array(), \"pop with empty array\");\n-\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n-\tTEST(t_split_empty_string(), \"split empty string\");\n-\tTEST(t_split_single_item(), \"split single item\");\n-\tTEST(t_split_multiple_items(), \"split multiple items\");\n-\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n-\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n-\tTEST(t_detach(), \"detach\");\n-\treturn test_done();\n-}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nindex 32a81299e9..82b7635e6a 100644\n--- a/t/unit-tests/unit-test.c\n+++ b/t/unit-tests/unit-test.c\n@@ -6,10 +6,9 @@ int cmd_main(int argc, const char **argv)\n \tint ret;\n \n \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n-\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tALLOC_ARRAY(argv_copy, argc + 1);\n \tCOPY_ARRAY(argv_copy, argv, argc);\n \targv_copy[argc++] = \"-t\";\n-\targv_copy[argc] = NULL;\n \n \tret = clar_test(argc, (char **) argv_copy);\n \n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501091","messageId":"ca09d19fd51cd4b3072b339f483b6b6d6e467b56.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 9/9] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:05:01Z","receivedAt":"2024-08-16T07:36:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the ctype tests to use the new clar unit testing framework.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                            |  2 +-\n t/unit-tests/{t-ctype.c => ctype.c} | 71 +++++++++++++++++++++++------\n 2 files changed, 59 insertions(+), 14 deletions(-)\n rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n\ndiff --git a/Makefile b/Makefile\nindex b0c9a79c55..000eca9f86 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,13 +1336,13 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += ctype\n UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n \n-UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n UNIT_TEST_PROGRAMS += t-mem-pool\ndiff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/ctype.c\nsimilarity index 71%\nrename from t/unit-tests/t-ctype.c\nrename to t/unit-tests/ctype.c\nindex d6ac1fe678..311df3a539 100644\n--- a/t/unit-tests/t-ctype.c\n+++ b/t/unit-tests/ctype.c\n@@ -1,18 +1,12 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n \n #define TEST_CHAR_CLASS(class, string) do { \\\n \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n-\tint skip = test__run_begin(); \\\n-\tif (!skip) { \\\n-\t\tfor (int i = 0; i < 256; i++) { \\\n-\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n-\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n-\t\t} \\\n-\t\tcheck(!class(EOF)); \\\n-\t} \\\n-\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n+\tfor (int i = 0; i < 256; i++) \\\n+\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n+\tcl_assert(!class(EOF)); \\\n } while (0)\n \n #define DIGIT \"0123456789\"\n@@ -33,21 +27,72 @@\n \t\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\" \\\n \t\"\\x7f\"\n \n-int cmd_main(int argc, const char **argv) {\n+void test_ctype__isspace(void)\n+{\n \tTEST_CHAR_CLASS(isspace, \" \\n\\r\\t\");\n+}\n+\n+void test_ctype__isdigit(void)\n+{\n \tTEST_CHAR_CLASS(isdigit, DIGIT);\n+}\n+\n+void test_ctype__isalpha(void)\n+{\n \tTEST_CHAR_CLASS(isalpha, LOWER UPPER);\n+}\n+\n+void test_ctype__isalnum(void)\n+{\n \tTEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);\n+}\n+\n+void test_ctype__is_glob_special(void)\n+{\n \tTEST_CHAR_CLASS(is_glob_special, \"*?[\\\\\");\n+}\n+\n+void test_ctype__is_regex_special(void)\n+{\n \tTEST_CHAR_CLASS(is_regex_special, \"$()*+.?[\\\\^{|\");\n+}\n+\n+void test_ctype__is_pathspec_magic(void)\n+{\n \tTEST_CHAR_CLASS(is_pathspec_magic, \"!\\\"#%&',-/:;<=>@_`~\");\n+}\n+\n+void test_ctype__isascii(void)\n+{\n \tTEST_CHAR_CLASS(isascii, ASCII);\n+}\n+\n+void test_ctype__islower(void)\n+{\n \tTEST_CHAR_CLASS(islower, LOWER);\n+}\n+\n+void test_ctype__isupper(void)\n+{\n \tTEST_CHAR_CLASS(isupper, UPPER);\n+}\n+\n+void test_ctype__iscntrl(void)\n+{\n \tTEST_CHAR_CLASS(iscntrl, CNTRL);\n+}\n+\n+void test_ctype__ispunct(void)\n+{\n \tTEST_CHAR_CLASS(ispunct, PUNCT);\n+}\n+\n+void test_ctype__isxdigit(void)\n+{\n \tTEST_CHAR_CLASS(isxdigit, DIGIT \"abcdefABCDEF\");\n-\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n+}\n \n-\treturn test_done();\n+void test_ctype__isprint(void)\n+{\n+\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n }\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501093","messageId":"369060793306daab1796ae3063b4eaacc287f12d.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 2/9] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:40Z","receivedAt":"2024-08-16T07:36:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Import the clar unit testing framework at commit 1516124 (Merge pull\nrequest #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |   4 +-\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 153 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n 20 files changed, 2943 insertions(+), 1 deletion(-)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n\ndiff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt\nindex 206037ffb1..5a432b7b29 100644\n--- a/Documentation/technical/unit-tests.txt\n+++ b/Documentation/technical/unit-tests.txt\n@@ -203,6 +203,7 @@ GitHub / GitLab stars to estimate this.\n :criterion: https://github.com/Snaipe/Criterion[Criterion]\n :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n :check: https://libcheck.github.io/check/[Check]\n+:clar: https://github.com/clar-test/clar[Clar]\n \n [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n |=====\n@@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n+{clar},{isc},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n |=====\n \n === Additional framework candidates\ndiff --git a/Makefile b/Makefile\nindex 3863e60b66..674b0ac4e1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1331,6 +1331,8 @@ THIRD_PARTY_SOURCES += compat/poll/%\n THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n@@ -3260,7 +3262,7 @@ $(SP_OBJ): %.sp: %.c %.o\n .PHONY: sparse\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n ifndef OPENSSL_SHA1\n \tEXCEPT_HDRS += sha1/openssl.h\n endif\ndiff --git a/t/unit-tests/clar/.github/workflows/ci.yml b/t/unit-tests/clar/.github/workflows/ci.yml\nnew file mode 100644\nindex 0000000000..b1ac2de460\n--- /dev/null\n+++ b/t/unit-tests/clar/.github/workflows/ci.yml\n@@ -0,0 +1,23 @@\n+name: CI\n+\n+on:\n+  push:\n+    branches: [ main ]\n+  pull_request:\n+    branches: [ main ]\n+\n+jobs:\n+  build:\n+    strategy:\n+      matrix:\n+        os: [ ubuntu-latest, macos-latest ]\n+\n+    runs-on: ${{ matrix.os }}\n+\n+    steps:\n+    - name: Check out\n+      uses: actions/checkout@v2\n+    - name: Build\n+      run: |\n+        cd test\n+        make\ndiff --git a/t/unit-tests/clar/COPYING b/t/unit-tests/clar/COPYING\nnew file mode 100644\nindex 0000000000..8983817f0c\n--- /dev/null\n+++ b/t/unit-tests/clar/COPYING\n@@ -0,0 +1,15 @@\n+ISC License\n+\n+Copyright (c) 2011-2015 Vicent Marti\n+\n+Permission to use, copy, modify, and/or distribute this software for any\n+purpose with or without fee is hereby granted, provided that the above\n+copyright notice and this permission notice appear in all copies.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ndiff --git a/t/unit-tests/clar/README.md b/t/unit-tests/clar/README.md\nnew file mode 100644\nindex 0000000000..a8961c5f10\n--- /dev/null\n+++ b/t/unit-tests/clar/README.md\n@@ -0,0 +1,329 @@\n+Come out and Clar\n+=================\n+\n+In Catalan, \"clar\" means clear, easy to perceive.  Using clar will make it\n+easy to test and make clear the quality of your code.\n+\n+> _Historical note_\n+>\n+> Originally the clar project was named \"clay\" because the word \"test\" has its\n+> roots in the latin word *\"testum\"*, meaning \"earthen pot\", and *\"testa\"*,\n+> meaning \"piece of burned clay\"?\n+>\n+> This is because historically, testing implied melting metal in a pot to\n+> check its quality.  Clay is what tests are made of.\n+\n+## Quick Usage Overview\n+\n+Clar is a minimal C unit testing framework. It's been written to replace the\n+old framework in [libgit2][libgit2], but it's both very versatile and\n+straightforward to use.\n+\n+Can you count to funk?\n+\n+- **Zero: Initialize test directory**\n+\n+    ~~~~ sh\n+    $ mkdir tests\n+    $ cp -r $CLAR_ROOT/clar* tests\n+    $ cp $CLAR_ROOT/test/clar_test.h tests\n+    $ cp $CLAR_ROOT/test/main.c.sample tests/main.c\n+    ~~~~\n+\n+- **One: Write some tests**\n+\n+    File: tests/adding.c:\n+\n+    ~~~~ c\n+    /* adding.c for the \"Adding\" suite */\n+    #include \"clar.h\"\n+\n+    static int *answer;\n+\n+    void test_adding__initialize(void)\n+    {\n+        answer = malloc(sizeof(int));\n+        cl_assert_(answer != NULL, \"No memory left?\");\n+        *answer = 42;\n+    }\n+\n+    void test_adding__cleanup(void)\n+    {\n+        free(answer);\n+    }\n+\n+    void test_adding__make_sure_math_still_works(void)\n+    {\n+        cl_assert_(5 > 3, \"Five should probably be greater than three\");\n+        cl_assert_(-5 < 2, \"Negative numbers are small, I think\");\n+        cl_assert_(*answer == 42, \"The universe is doing OK. And the initializer too.\");\n+    }\n+    ~~~~~\n+\n+- **Two: Build the test executable**\n+\n+    ~~~~ sh\n+    $ cd tests\n+    $ $CLAR_PATH/generate.py .\n+    Written `clar.suite` (1 suites)\n+    $ gcc -I. clar.c main.c adding.c -o testit\n+    ~~~~\n+\n+- **Funk: Funk it.**\n+\n+    ~~~~ sh\n+    $ ./testit\n+    ~~~~\n+\n+## The Clar Test Suite\n+\n+Writing a test suite is pretty straightforward. Each test suite is a `*.c`\n+file with a descriptive name: this encourages modularity.\n+\n+Each test suite has optional initialize and cleanup methods. These methods\n+will be called before and after running **each** test in the suite, even if\n+such test fails. As a rule of thumb, if a test needs a different initializer\n+or cleanup method than another test in the same module, that means it\n+doesn't belong in that module. Keep that in mind when grouping tests\n+together.\n+\n+The `initialize` and `cleanup` methods have the following syntax, with\n+`suitename` being the current suite name, e.g. `adding` for the `adding.c`\n+suite.\n+\n+~~~~ c\n+void test_suitename__initialize(void)\n+{\n+    /* init */\n+}\n+\n+void test_suitename__cleanup(void)\n+{\n+    /* cleanup */\n+}\n+~~~~\n+\n+These methods are encouraged to use static, global variables to store the state\n+that will be used by all tests inside the suite.\n+\n+~~~~ c\n+static git_repository *_repository;\n+\n+void test_status__initialize(void)\n+{\n+    create_tmp_repo(STATUS_REPO);\n+    git_repository_open(_repository, STATUS_REPO);\n+}\n+\n+void test_status__cleanup(void)\n+{\n+    git_repository_close(_repository);\n+    git_path_rm(STATUS_REPO);\n+}\n+\n+void test_status__simple_test(void)\n+{\n+    /* do something with _repository */\n+}\n+~~~~\n+\n+Writing the actual tests is just as straightforward. Tests have the\n+`void test_suitename__test_name(void)` signature, and they should **not**\n+be static. Clar will automatically detect and list them.\n+\n+Tests are run as they appear on their original suites: they have no return\n+value. A test is considered \"passed\" if it doesn't raise any errors. Check\n+the \"Clar API\" section to see the various helper functions to check and\n+raise errors during test execution.\n+\n+__Caution:__ If you use assertions inside of `test_suitename__initialize`,\n+make sure that you do not rely on `__initialize` being completely run\n+inside your `test_suitename__cleanup` function. Otherwise you might\n+encounter ressource cleanup twice.\n+\n+## How does Clar work?\n+\n+To use Clar:\n+\n+1. copy the Clar boilerplate to your test directory\n+2. copy (and probably modify) the sample `main.c` (from\n+   `$CLAR_PATH/test/main.c.sample`)\n+3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and\n+   write out the test suite metadata.\n+4. compile your test files and the Clar boilerplate into a single test\n+   executable\n+5. run the executable to test!\n+\n+The Clar boilerplate gives you a set of useful test assertions and features\n+(like accessing or making sandbox copies of fixture data).  It consists of\n+the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory.\n+You should not need to edit these files.\n+\n+The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes\n+`clar_test(argc, argv)` to run the tests.  Usually, you will edit this file\n+to perform any framework specific initialization and teardown that you need.\n+\n+The Clar mixer (`generate.py`) recursively scans your test directory for\n+any `.c` files, parses them, and writes the `clar.suite` file with all of\n+the metadata about your tests.  When you build, the `clar.suite` file is\n+included into `clar.c`.\n+\n+The mixer can be run with **Python 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and PyPy 1.6**.\n+\n+Commandline usage of the mixer is as follows:\n+\n+    $ ./generate.py .\n+\n+Where `.` is the folder where all the test suites can be found. The mixer\n+will automatically locate all the relevant source files and build the\n+testing metadata. The metadata will be written to `clar.suite`, in the same\n+folder as all the test suites. This file is included by `clar.c` and so\n+must be accessible via `#include` when building the test executable.\n+\n+    $ gcc -I. clar.c main.c suite1.c test2.c -o run_tests\n+\n+**Note that the Clar mixer only needs to be ran when adding new tests to a\n+suite, in order to regenerate the metadata**. As a result, the `clar.suite`\n+file can be checked into version control if you wish to be able to build\n+your test suite without having to re-run the mixer.\n+\n+This is handy when e.g. generating tests in a local computer, and then\n+building and testing them on an embedded device or a platform where Python\n+is not available.\n+\n+### Fixtures\n+\n+Clar can create sandboxed fixtures for you to use in your test. You'll need to compile *clar.c* with an additional `CFLAG`, `-DCLAR_FIXTURE_PATH`. This should be an absolute path to your fixtures directory.\n+\n+Once that's done, you can use the fixture API as defined below.\n+\n+## The Clar API\n+\n+Clar makes the following methods available from all functions in a test\n+suite.\n+\n+-   `cl_must_pass(call)`, `cl_must_pass_(call, message)`: Verify that the given\n+    function call passes, in the POSIX sense (returns a value greater or equal\n+    to 0).\n+\n+-   `cl_must_fail(call)`, `cl_must_fail_(call, message)`: Verify that the given\n+    function call fails, in the POSIX sense (returns a value less than 0).\n+\n+-   `cl_assert(expr)`, `cl_assert_(expr, message)`: Verify that `expr` is true.\n+\n+-   `cl_check_pass(call)`, `cl_check_pass_(call, message)`: Verify that the\n+    given function call passes, in the POSIX sense (returns a value greater or\n+    equal to 0). If the function call doesn't succeed, a test failure will be\n+    logged but the test's execution will continue.\n+\n+-   `cl_check_fail(call)`, `cl_check_fail_(call, message)`: Verify that the\n+    given function call fails, in the POSIX sense (returns a value less than\n+    0). If the function call doesn't fail, a test failure will be logged but\n+    the test's execution will continue.\n+\n+-   `cl_check(expr)`: Verify that `expr` is true. If `expr` is not\n+    true, a test failure will be logged but the test's execution will continue.\n+\n+-   `cl_fail(message)`: Fail the current test with the given message.\n+\n+-   `cl_warning(message)`: Issue a warning. This warning will be\n+    logged as a test failure but the test's execution will continue.\n+\n+-   `cl_set_cleanup(void (*cleanup)(void *), void *opaque)`: Set the cleanup\n+    method for a single test. This method will be called with `opaque` as its\n+    argument before the test returns (even if the test has failed).\n+    If a global cleanup method is also available, the local cleanup will be\n+    called first, and then the global.\n+\n+-   `cl_assert_equal_i(int,int)`: Verify that two integer values are equal.\n+    The advantage of this over a simple `cl_assert` is that it will format\n+    a much nicer error report if the values are not equal.\n+\n+-   `cl_assert_equal_s(const char *,const char *)`: Verify that two strings\n+    are equal.  The expected value can also be NULL and this will correctly\n+    test for that.\n+\n+-   `cl_fixture_sandbox(const char *)`: Sets up a sandbox for a fixture\n+    so that you can mutate the file directly.\n+\n+-   `cl_fixture_cleanup(const char *)`: Tears down the previous fixture\n+    sandbox.\n+\n+-   `cl_fixture(const char *)`: Gets the full path to a fixture file.\n+\n+Please do note that these methods are *always* available whilst running a\n+test, even when calling auxiliary/static functions inside the same file.\n+\n+It's strongly encouraged to perform test assertions in auxiliary methods,\n+instead of returning error values. This is considered good Clar style.\n+\n+Style Example:\n+\n+~~~~ c\n+/*\n+ * Bad style: auxiliary functions return an error code\n+ */\n+\n+static int check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    if (aux == NULL)\n+        return -1;\n+\n+    return strcmp(my_function(aux), str) == 0 ? 0 : -1;\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    cl_must_pass_(\n+        check_string(\"foo\"),\n+        \"String differs after processing\"\n+    );\n+\n+    cl_must_pass_(\n+        check_string(\"bar\"),\n+        \"String differs after processing\"\n+    );\n+}\n+~~~~\n+\n+~~~~ c\n+/*\n+ * Good style: auxiliary functions perform assertions\n+ */\n+\n+static void check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    cl_assert_(\n+        aux != NULL,\n+        \"String processing failed\"\n+    );\n+\n+    cl_assert_(\n+        strcmp(my_function(aux), str) == 0,\n+        \"String differs after processing\"\n+    );\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    check_string(\"foo\");\n+    check_string(\"bar\");\n+}\n+~~~~\n+\n+About Clar\n+==========\n+\n+Clar has been written from scratch by [Vicent Martí](https://github.com/vmg),\n+to replace the old testing framework in [libgit2][libgit2].\n+\n+Do you know what languages are *in* on the SF startup scene? Node.js *and*\n+Latin.  Follow [@vmg](https://www.twitter.com/vmg) on Twitter to\n+receive more lessons on word etymology. You can be hip too.\n+\n+\n+[libgit2]: https://github.com/libgit2/libgit2\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nnew file mode 100644\nindex 0000000000..3fc2c76815\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.c\n@@ -0,0 +1,842 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#include <assert.h>\n+#include <setjmp.h>\n+#include <stdlib.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <math.h>\n+#include <stdarg.h>\n+#include <wchar.h>\n+#include <time.h>\n+\n+/* required for sandboxing */\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#ifdef _WIN32\n+#\tinclude <windows.h>\n+#\tinclude <io.h>\n+#\tinclude <shellapi.h>\n+#\tinclude <direct.h>\n+\n+#\tdefine _MAIN_CC __cdecl\n+\n+#\tifndef stat\n+#\t\tdefine stat(path, st) _stat(path, st)\n+#\tendif\n+#\tifndef mkdir\n+#\t\tdefine mkdir(path, mode) _mkdir(path)\n+#\tendif\n+#\tifndef chdir\n+#\t\tdefine chdir(path) _chdir(path)\n+#\tendif\n+#\tifndef access\n+#\t\tdefine access(path, mode) _access(path, mode)\n+#\tendif\n+#\tifndef strdup\n+#\t\tdefine strdup(str) _strdup(str)\n+#\tendif\n+#\tifndef strcasecmp\n+#\t\tdefine strcasecmp(a,b) _stricmp(a,b)\n+#\tendif\n+\n+#\tifndef __MINGW32__\n+#\t\tpragma comment(lib, \"shell32\")\n+#\t\tifndef strncpy\n+#\t\t\tdefine strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)\n+#\t\tendif\n+#\t\tifndef W_OK\n+#\t\t\tdefine W_OK 02\n+#\t\tendif\n+#\t\tifndef S_ISDIR\n+#\t\t\tdefine S_ISDIR(x) ((x & _S_IFDIR) != 0)\n+#\t\tendif\n+#\t\tdefine p_snprintf(buf,sz,fmt,...) _snprintf_s(buf,sz,_TRUNCATE,fmt,__VA_ARGS__)\n+#\telse\n+#\t\tdefine p_snprintf snprintf\n+#\tendif\n+\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"Iu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"Ix\"\n+#\tendif\n+\n+#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+\ttypedef struct stat STAT_T;\n+#\telse\n+\ttypedef struct _stat STAT_T;\n+#\tendif\n+#else\n+#\tinclude <sys/wait.h> /* waitpid(2) */\n+#\tinclude <unistd.h>\n+#\tdefine _MAIN_CC\n+#\tdefine p_snprintf snprintf\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"zu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"zx\"\n+#\tendif\n+\ttypedef struct stat STAT_T;\n+#endif\n+\n+#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n+\n+#include \"clar.h\"\n+\n+static void fs_rm(const char *_source);\n+static void fs_copy(const char *_source, const char *dest);\n+\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name);\n+#endif\n+\n+struct clar_error {\n+\tconst char *file;\n+\tconst char *function;\n+\tsize_t line_number;\n+\tconst char *error_msg;\n+\tchar *description;\n+\n+\tstruct clar_error *next;\n+};\n+\n+struct clar_explicit {\n+\tsize_t suite_idx;\n+\tconst char *filter;\n+\n+\tstruct clar_explicit *next;\n+};\n+\n+struct clar_report {\n+\tconst char *test;\n+\tint test_number;\n+\tconst char *suite;\n+\n+\tenum cl_test_status status;\n+\ttime_t start;\n+\tdouble elapsed;\n+\n+\tstruct clar_error *errors;\n+\tstruct clar_error *last_error;\n+\n+\tstruct clar_report *next;\n+};\n+\n+struct clar_summary {\n+\tconst char *filename;\n+\tFILE *fp;\n+};\n+\n+static struct {\n+\tenum cl_test_status test_status;\n+\n+\tconst char *active_test;\n+\tconst char *active_suite;\n+\n+\tint total_skipped;\n+\tint total_errors;\n+\n+\tint tests_ran;\n+\tint suites_ran;\n+\n+\tenum cl_output_format output_format;\n+\n+\tint report_errors_only;\n+\tint exit_on_error;\n+\tint verbosity;\n+\n+\tint write_summary;\n+\tchar *summary_filename;\n+\tstruct clar_summary *summary;\n+\n+\tstruct clar_explicit *explicit;\n+\tstruct clar_explicit *last_explicit;\n+\n+\tstruct clar_report *reports;\n+\tstruct clar_report *last_report;\n+\n+\tvoid (*local_cleanup)(void *);\n+\tvoid *local_cleanup_payload;\n+\n+\tjmp_buf trampoline;\n+\tint trampoline_enabled;\n+\n+\tcl_trace_cb *pfn_trace_cb;\n+\tvoid *trace_payload;\n+\n+} _clar;\n+\n+struct clar_func {\n+\tconst char *name;\n+\tvoid (*ptr)(void);\n+};\n+\n+struct clar_suite {\n+\tconst char *name;\n+\tstruct clar_func initialize;\n+\tstruct clar_func cleanup;\n+\tconst struct clar_func *tests;\n+\tsize_t test_count;\n+\tint enabled;\n+};\n+\n+/* From clar_print_*.c */\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n+static void clar_print_onsuite(const char *suite_name, int suite_index);\n+static void clar_print_onabort(const char *msg, ...);\n+\n+/* From clar_sandbox.c */\n+static void clar_unsandbox(void);\n+static int clar_sandbox(void);\n+\n+/* From summary.h */\n+static struct clar_summary *clar_summary_init(const char *filename);\n+static int clar_summary_shutdown(struct clar_summary *fp);\n+\n+/* Load the declarations for the test suite */\n+#include \"clar.suite\"\n+\n+\n+#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n+\t} while (0)\n+\n+void cl_trace_register(cl_trace_cb *cb, void *payload)\n+{\n+\t_clar.pfn_trace_cb = cb;\n+\t_clar.trace_payload = payload;\n+}\n+\n+\n+/* Core test functions */\n+static void\n+clar_report_errors(struct clar_report *report)\n+{\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (error = report->errors; error; error = error->next)\n+\t\tclar_print_error(i++, _clar.last_report, error);\n+}\n+\n+static void\n+clar_report_all(void)\n+{\n+\tstruct clar_report *report;\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (report = _clar.reports; report; report = report->next) {\n+\t\tif (report->status != CL_TEST_FAILURE)\n+\t\t\tcontinue;\n+\n+\t\tfor (error = report->errors; error; error = error->next)\n+\t\t\tclar_print_error(i++, report, error);\n+\t}\n+}\n+\n+#ifdef WIN32\n+# define clar_time DWORD\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\t*out = GetTickCount();\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)*end - (double)*start) / 1000;\n+}\n+#else\n+# include <sys/time.h>\n+\n+# define clar_time struct timeval\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\tstruct timezone tz;\n+\n+\tgettimeofday(out, &tz);\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n+\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n+}\n+#endif\n+\n+static void\n+clar_run_test(\n+\tconst struct clar_suite *suite,\n+\tconst struct clar_func *test,\n+\tconst struct clar_func *initialize,\n+\tconst struct clar_func *cleanup)\n+{\n+\tclar_time start, end;\n+\n+\t_clar.trampoline_enabled = 1;\n+\n+\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n+\n+\t_clar.last_report->start = time(NULL);\n+\tclar_time_now(&start);\n+\n+\tif (setjmp(_clar.trampoline) == 0) {\n+\t\tif (initialize->ptr != NULL)\n+\t\t\tinitialize->ptr();\n+\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n+\t\ttest->ptr();\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n+\t}\n+\n+\tclar_time_now(&end);\n+\n+\t_clar.trampoline_enabled = 0;\n+\n+\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n+\t\t_clar.last_report->status = CL_TEST_OK;\n+\n+\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n+\n+\tif (_clar.local_cleanup != NULL)\n+\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n+\n+\tif (cleanup->ptr != NULL)\n+\t\tcleanup->ptr();\n+\n+\tCL_TRACE(CL_TRACE__TEST__END);\n+\n+\t_clar.tests_ran++;\n+\n+\t/* remove any local-set cleanup methods */\n+\t_clar.local_cleanup = NULL;\n+\t_clar.local_cleanup_payload = NULL;\n+\n+\tif (_clar.report_errors_only) {\n+\t\tclar_report_errors(_clar.last_report);\n+\t} else {\n+\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n+\t}\n+}\n+\n+static void\n+clar_run_suite(const struct clar_suite *suite, const char *filter)\n+{\n+\tconst struct clar_func *test = suite->tests;\n+\tsize_t i, matchlen;\n+\tstruct clar_report *report;\n+\tint exact = 0;\n+\n+\tif (!suite->enabled)\n+\t\treturn;\n+\n+\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\treturn;\n+\n+\tif (!_clar.report_errors_only)\n+\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n+\n+\t_clar.active_suite = suite->name;\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n+\n+\tif (filter) {\n+\t\tsize_t suitelen = strlen(suite->name);\n+\t\tmatchlen = strlen(filter);\n+\t\tif (matchlen <= suitelen) {\n+\t\t\tfilter = NULL;\n+\t\t} else {\n+\t\t\tfilter += suitelen;\n+\t\t\twhile (*filter == ':')\n+\t\t\t\t++filter;\n+\t\t\tmatchlen = strlen(filter);\n+\n+\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n+\t\t\t\texact = 1;\n+\t\t\t\tmatchlen--;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (i = 0; i < suite->test_count; ++i) {\n+\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n+\t\t\tcontinue;\n+\n+\t\tif (exact && strlen(test[i].name) != matchlen)\n+\t\t\tcontinue;\n+\n+\t\t_clar.active_test = test[i].name;\n+\n+\t\treport = calloc(1, sizeof(struct clar_report));\n+\t\treport->suite = _clar.active_suite;\n+\t\treport->test = _clar.active_test;\n+\t\treport->test_number = _clar.tests_ran;\n+\t\treport->status = CL_TEST_NOTRUN;\n+\n+\t\tif (_clar.reports == NULL)\n+\t\t\t_clar.reports = report;\n+\n+\t\tif (_clar.last_report != NULL)\n+\t\t\t_clar.last_report->next = report;\n+\n+\t\t_clar.last_report = report;\n+\n+\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n+\n+\t\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\t\treturn;\n+\t}\n+\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_END);\n+}\n+\n+static void\n+clar_usage(const char *arg)\n+{\n+\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n+\tprintf(\"Options:\\n\");\n+\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n+\tprintf(\"  -iname        Include the suite with `name`\\n\");\n+\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n+\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n+\tprintf(\"  -q            Only report tests that had an error\\n\");\n+\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n+\tprintf(\"  -t            Display results in tap format\\n\");\n+\tprintf(\"  -l            Print suite names\\n\");\n+\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n+\texit(-1);\n+}\n+\n+static void\n+clar_parse_args(int argc, char **argv)\n+{\n+\tint i;\n+\n+\t/* Verify options before execute */\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tif (argument[0] != '-' || argument[1] == '\\0'\n+\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n+\t\t\tclar_usage(argv[0]);\n+\t\t}\n+\t}\n+\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tswitch (argument[1]) {\n+\t\tcase 's':\n+\t\tcase 'i':\n+\t\tcase 'x': { /* given suite name */\n+\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n+\t\t\tchar action = argument[1];\n+\t\t\tsize_t j, arglen, suitelen, cmplen;\n+\n+\t\t\targument += offset;\n+\t\t\targlen = strlen(argument);\n+\n+\t\t\tif (arglen == 0)\n+\t\t\t\tclar_usage(argv[0]);\n+\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n+\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n+\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n+\n+\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n+\t\t\t\t\tint exact = (arglen >= suitelen);\n+\n+\t\t\t\t\t/* Do we have a real suite prefix separated by a\n+\t\t\t\t\t * trailing '::' or just a matching substring? */\n+\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n+\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n+\t\t\t\t\t    continue;\n+\n+\t\t\t\t\t++found;\n+\n+\t\t\t\t\tif (!exact)\n+\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n+\n+\t\t\t\t\tswitch (action) {\n+\t\t\t\t\tcase 's': {\n+\t\t\t\t\t\tstruct clar_explicit *explicit =\n+\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n+\t\t\t\t\t\tassert(explicit);\n+\n+\t\t\t\t\t\texplicit->suite_idx = j;\n+\t\t\t\t\t\texplicit->filter = argument;\n+\n+\t\t\t\t\t\tif (_clar.explicit == NULL)\n+\t\t\t\t\t\t\t_clar.explicit = explicit;\n+\n+\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n+\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n+\n+\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n+\t\t\t\t\t\t_clar.last_explicit = explicit;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n+\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif (exact)\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (!found) {\n+\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n+\t\t\t\texit(-1);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase 'q':\n+\t\t\t_clar.report_errors_only = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 'Q':\n+\t\t\t_clar.exit_on_error = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 't':\n+\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n+\t\t\tbreak;\n+\n+\t\tcase 'l': {\n+\t\t\tsize_t j;\n+\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n+\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n+\n+\t\t\texit(0);\n+\t\t}\n+\n+\t\tcase 'v':\n+\t\t\t_clar.verbosity++;\n+\t\t\tbreak;\n+\n+\t\tcase 'r':\n+\t\t\t_clar.write_summary = 1;\n+\t\t\tfree(_clar.summary_filename);\n+\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tassert(!\"Unexpected commandline argument!\");\n+\t\t}\n+\t}\n+}\n+\n+void\n+clar_test_init(int argc, char **argv)\n+{\n+\tconst char *summary_env;\n+\n+\tif (argc > 1)\n+\t\tclar_parse_args(argc, argv);\n+\n+\tclar_print_init(\n+\t\t(int)_clar_callback_count,\n+\t\t(int)_clar_suite_count,\n+\t\t\"\"\n+\t);\n+\n+\tif (!_clar.summary_filename &&\n+\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n+\t\t_clar.write_summary = 1;\n+\t\t_clar.summary_filename = strdup(summary_env);\n+\t}\n+\n+\tif (_clar.write_summary && !_clar.summary_filename)\n+\t\t_clar.summary_filename = strdup(\"summary.xml\");\n+\n+\tif (_clar.write_summary &&\n+\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n+\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tif (clar_sandbox() < 0) {\n+\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n+\t\texit(-1);\n+\t}\n+}\n+\n+int\n+clar_test_run(void)\n+{\n+\tsize_t i;\n+\tstruct clar_explicit *explicit;\n+\n+\tif (_clar.explicit) {\n+\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n+\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n+\t} else {\n+\t\tfor (i = 0; i < _clar_suite_count; ++i)\n+\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n+\t}\n+\n+\treturn _clar.total_errors;\n+}\n+\n+void\n+clar_test_shutdown(void)\n+{\n+\tstruct clar_explicit *explicit, *explicit_next;\n+\tstruct clar_report *report, *report_next;\n+\n+\tclar_print_shutdown(\n+\t\t_clar.tests_ran,\n+\t\t(int)_clar_suite_count,\n+\t\t_clar.total_errors\n+\t);\n+\n+\tclar_unsandbox();\n+\n+\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n+\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n+\t\texplicit_next = explicit->next;\n+\t\tfree(explicit);\n+\t}\n+\n+\tfor (report = _clar.reports; report; report = report_next) {\n+\t\treport_next = report->next;\n+\t\tfree(report);\n+\t}\n+\n+\tfree(_clar.summary_filename);\n+}\n+\n+int\n+clar_test(int argc, char **argv)\n+{\n+\tint errors;\n+\n+\tclar_test_init(argc, argv);\n+\terrors = clar_test_run();\n+\tclar_test_shutdown();\n+\n+\treturn errors;\n+}\n+\n+static void abort_test(void)\n+{\n+\tif (!_clar.trampoline_enabled) {\n+\t\tclar_print_onabort(\n+\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n+\t\tclar_report_errors(_clar.last_report);\n+\t\texit(-1);\n+\t}\n+\n+\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n+\tlongjmp(_clar.trampoline, -1);\n+}\n+\n+void clar__skip(void)\n+{\n+\t_clar.last_report->status = CL_TEST_SKIP;\n+\t_clar.total_skipped++;\n+\tabort_test();\n+}\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n+\n+\tif (_clar.last_report->errors == NULL)\n+\t\t_clar.last_report->errors = error;\n+\n+\tif (_clar.last_report->last_error != NULL)\n+\t\t_clar.last_report->last_error->next = error;\n+\n+\t_clar.last_report->last_error = error;\n+\n+\terror->file = file;\n+\terror->function = function;\n+\terror->line_number = line;\n+\terror->error_msg = error_msg;\n+\n+\tif (description != NULL)\n+\t\terror->description = strdup(description);\n+\n+\t_clar.total_errors++;\n+\t_clar.last_report->status = CL_TEST_FAILURE;\n+\n+\tif (should_abort)\n+\t\tabort_test();\n+}\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tif (condition)\n+\t\treturn;\n+\n+\tclar__fail(file, function, line, error_msg, description, should_abort);\n+}\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...)\n+{\n+\tva_list args;\n+\tchar buf[4096];\n+\tint is_equal = 1;\n+\n+\tva_start(args, fmt);\n+\n+\tif (!strcmp(\"%s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n+\t\t\t\t\ts1, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n+\t\t\t\t\tlen, s1, len, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n+\t\t\t\t\twcs1, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n+\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n+\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n+\t\tis_equal = (sz1 == sz2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%p\", fmt)) {\n+\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n+\t\tis_equal = (p1 == p2);\n+\t\tif (!is_equal)\n+\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n+\t}\n+\telse {\n+\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n+\t\tis_equal = (i1 == i2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n+\t\t}\n+\t}\n+\n+\tva_end(args);\n+\n+\tif (!is_equal)\n+\t\tclar__fail(file, function, line, err, buf, should_abort);\n+}\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n+{\n+\t_clar.local_cleanup = cleanup;\n+\t_clar.local_cleanup_payload = opaque;\n+}\n+\n+#include \"clar/sandbox.h\"\n+#include \"clar/fixtures.h\"\n+#include \"clar/fs.h\"\n+#include \"clar/print.h\"\n+#include \"clar/summary.h\"\ndiff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\nnew file mode 100644\nindex 0000000000..8c22382bd5\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.h\n@@ -0,0 +1,173 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST_H__\n+#define __CLAR_TEST_H__\n+\n+#include <stdlib.h>\n+\n+enum cl_test_status {\n+\tCL_TEST_OK,\n+\tCL_TEST_FAILURE,\n+\tCL_TEST_SKIP,\n+\tCL_TEST_NOTRUN,\n+};\n+\n+enum cl_output_format {\n+\tCL_OUTPUT_CLAP,\n+\tCL_OUTPUT_TAP,\n+};\n+\n+/** Setup clar environment */\n+void clar_test_init(int argc, char *argv[]);\n+int clar_test_run(void);\n+void clar_test_shutdown(void);\n+\n+/** One shot setup & run */\n+int clar_test(int argc, char *argv[]);\n+\n+const char *clar_sandbox_path(void);\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n+void cl_fs_cleanup(void);\n+\n+/**\n+ * cl_trace_* is a hook to provide a simple global tracing\n+ * mechanism.\n+ *\n+ * The goal here is to let main() provide clar-proper\n+ * with a callback to optionally write log info for\n+ * test operations into the same stream used by their\n+ * actual tests.  This would let them print test names\n+ * and maybe performance data as they choose.\n+ *\n+ * The goal is NOT to alter the flow of control or to\n+ * override test selection/skipping.  (So the callback\n+ * does not return a value.)\n+ *\n+ * The goal is NOT to duplicate the existing\n+ * pass/fail/skip reporting.  (So the callback\n+ * does not accept a status/errorcode argument.)\n+ *\n+ */\n+typedef enum cl_trace_event {\n+\tCL_TRACE__SUITE_BEGIN,\n+\tCL_TRACE__SUITE_END,\n+\tCL_TRACE__TEST__BEGIN,\n+\tCL_TRACE__TEST__END,\n+\tCL_TRACE__TEST__RUN_BEGIN,\n+\tCL_TRACE__TEST__RUN_END,\n+\tCL_TRACE__TEST__LONGJMP,\n+} cl_trace_event;\n+\n+typedef void (cl_trace_cb)(\n+\tcl_trace_event ev,\n+\tconst char *suite_name,\n+\tconst char *test_name,\n+\tvoid *payload);\n+\n+/**\n+ * Register a callback into CLAR to send global trace events.\n+ * Pass NULL to disable.\n+ */\n+void cl_trace_register(cl_trace_cb *cb, void *payload);\n+\n+\n+#ifdef CLAR_FIXTURE_PATH\n+const char *cl_fixture(const char *fixture_name);\n+void cl_fixture_sandbox(const char *fixture_name);\n+void cl_fixture_cleanup(const char *fixture_name);\n+const char *cl_fixture_basename(const char *fixture_name);\n+#endif\n+\n+/**\n+ * Assertion macros with explicit error message\n+ */\n+#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n+#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n+#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n+\n+/**\n+ * Check macros with explicit error message\n+ */\n+#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n+#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n+#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n+\n+/**\n+ * Assertion macros with no error message\n+ */\n+#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n+#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n+#define cl_assert(expr) cl_assert_(expr, NULL)\n+\n+/**\n+ * Check macros with no error message\n+ */\n+#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n+#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n+#define cl_check(expr) cl_check_(expr, NULL)\n+\n+/**\n+ * Forced failure/warning\n+ */\n+#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n+#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n+\n+#define cl_skip() clar__skip()\n+\n+/**\n+ * Typed assertion macros\n+ */\n+#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n+#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n+\n+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n+\n+#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n+#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n+\n+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+\n+#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n+#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n+#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n+\n+#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n+\n+#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n+\n+void clar__skip(void);\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...);\n+\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\nnew file mode 100644\nindex 0000000000..6ec6423484\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fixtures.h\n@@ -0,0 +1,50 @@\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name)\n+{\n+\tstatic char _path[4096];\n+\tsize_t root_len;\n+\n+\troot_len = strlen(base);\n+\tstrncpy(_path, base, sizeof(_path));\n+\n+\tif (_path[root_len - 1] != '/')\n+\t\t_path[root_len++] = '/';\n+\n+\tif (fixture_name[0] == '/')\n+\t\tfixture_name++;\n+\n+\tstrncpy(_path + root_len,\n+\t\tfixture_name,\n+\t\tsizeof(_path) - root_len);\n+\n+\treturn _path;\n+}\n+\n+const char *cl_fixture(const char *fixture_name)\n+{\n+\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n+}\n+\n+void cl_fixture_sandbox(const char *fixture_name)\n+{\n+\tfs_copy(cl_fixture(fixture_name), _clar_path);\n+}\n+\n+const char *cl_fixture_basename(const char *fixture_name)\n+{\n+\tconst char *p;\n+\n+\tfor (p = fixture_name; *p; p++) {\n+\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n+\t\t\tfixture_name = p+1;\n+\t}\n+\n+\treturn fixture_name;\n+}\n+\n+void cl_fixture_cleanup(const char *fixture_name)\n+{\n+\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nnew file mode 100644\nindex 0000000000..3e39890bd3\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -0,0 +1,522 @@\n+/*\n+ * By default, use a read/write loop to copy files on POSIX systems.\n+ * On Linux, use sendfile by default as it's slightly faster.  On\n+ * macOS, we avoid fcopyfile by default because it's slightly slower.\n+ */\n+#undef USE_FCOPYFILE\n+#define USE_SENDFILE 1\n+\n+#ifdef _WIN32\n+\n+#ifdef CLAR_WIN32_LONGPATHS\n+# define CLAR_MAX_PATH 4096\n+#else\n+# define CLAR_MAX_PATH MAX_PATH\n+#endif\n+\n+#define RM_RETRY_COUNT\t5\n+#define RM_RETRY_DELAY\t10\n+\n+#ifdef __MINGW32__\n+\n+/* These security-enhanced functions are not available\n+ * in MinGW, so just use the vanilla ones */\n+#define wcscpy_s(a, b, c) wcscpy((a), (c))\n+#define wcscat_s(a, b, c) wcscat((a), (c))\n+\n+#endif /* __MINGW32__ */\n+\n+static int\n+fs__dotordotdot(WCHAR *_tocheck)\n+{\n+\treturn _tocheck[0] == '.' &&\n+\t\t(_tocheck[1] == '\\0' ||\n+\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n+}\n+\n+static int\n+fs_rmdir_rmdir(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\n+\twhile (!RemoveDirectoryW(_wpath)) {\n+\t\t/* Only retry when we have retries remaining, and the\n+\t\t * error was ERROR_DIR_NOT_EMPTY. */\n+\t\tif (retries++ > RM_RETRY_COUNT ||\n+\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n+\t\t\treturn -1;\n+\n+\t\t/* Give whatever has a handle to a child item some time\n+\t\t * to release it before trying again */\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void translate_path(WCHAR *path, size_t path_size)\n+{\n+    size_t path_len, i;\n+\n+    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n+\treturn;\n+\n+    path_len = wcslen(path);\n+    cl_assert(path_size > path_len + 4);\n+\n+    for (i = path_len; i > 0; i--) {\n+\tWCHAR c = path[i - 1];\n+\n+\tif (c == L'/')\n+\t    path[i + 3] = L'\\\\';\n+\telse\n+\t    path[i + 3] = path[i - 1];\n+    }\n+\n+    path[0] = L'\\\\';\n+    path[1] = L'\\\\';\n+    path[2] = L'?';\n+    path[3] = L'\\\\';\n+    path[path_len + 4] = L'\\0';\n+}\n+\n+static void\n+fs_rmdir_helper(WCHAR *_wsource)\n+{\n+\tWCHAR buffer[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buffer_prefix_len;\n+\n+\t/* Set up the buffer and capture the length */\n+\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n+\ttranslate_path(buffer, CLAR_MAX_PATH);\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n+\tbuffer_prefix_len = wcslen(buffer);\n+\n+\t/* FindFirstFile needs a wildcard to match multiple items */\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buffer, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_rmdir_helper(buffer);\n+\t\telse {\n+\t\t\t/* If set, the +R bit must be cleared before deleting */\n+\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n+\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\t\tcl_assert(DeleteFileW(buffer));\n+\t\t}\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+\n+\t/* Now that the directory is empty, remove it */\n+\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n+}\n+\n+static int\n+fs_rm_wait(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\tDWORD last_error;\n+\n+\tdo {\n+\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n+\t\t\tlast_error = GetLastError();\n+\t\telse\n+\t\t\tlast_error = ERROR_SUCCESS;\n+\n+\t\t/* Is the item gone? */\n+\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n+\t\t\tERROR_PATH_NOT_FOUND == last_error)\n+\t\t\treturn 0;\n+\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\twhile (retries++ <= RM_RETRY_COUNT);\n+\n+\treturn -1;\n+}\n+\n+static void\n+fs_rm(const char *_source)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH];\n+\tDWORD attrs;\n+\n+\t/* The input path is UTF-8. Convert it to wide characters\n+\t * for use with the Windows API */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1, /* Indicates NULL termination */\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\n+\t/* Does the item exist? If not, we have no work to do */\n+\tattrs = GetFileAttributesW(wsource);\n+\n+\tif (INVALID_FILE_ATTRIBUTES == attrs)\n+\t\treturn;\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n+\t\tfs_rmdir_helper(wsource);\n+\telse {\n+\t\t/* The item is a file. Strip the +R bit */\n+\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n+\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\tcl_assert(DeleteFileW(wsource));\n+\t}\n+\n+\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n+\tcl_assert(0 == fs_rm_wait(wsource));\n+}\n+\n+static void\n+fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n+{\n+\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n+\n+\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_source, CLAR_MAX_PATH);\n+\tbuf_source_prefix_len = wcslen(buf_source);\n+\n+\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n+\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n+\tbuf_dest_prefix_len = wcslen(buf_dest);\n+\n+\t/* Get an enumerator for the items in the source. */\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buf_source, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\t/* Create the target directory. */\n+\tcl_assert(CreateDirectoryW(_wdest, NULL));\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n+\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_copydir_helper(buf_source, buf_dest);\n+\t\telse\n+\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+}\n+\n+static void\n+fs_copy(const char *_source, const char *_dest)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n+\tDWORD source_attrs, dest_attrs;\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\n+\t/* The input paths are UTF-8. Convert them to wide characters\n+\t * for use with the Windows API. */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1,\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_dest,\n+\t\t\t\t-1,\n+\t\t\t\twdest,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\ttranslate_path(wdest, CLAR_MAX_PATH);\n+\n+\t/* Check the source for existence */\n+\tsource_attrs = GetFileAttributesW(wsource);\n+\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n+\n+\t/* Check the target for existence */\n+\tdest_attrs = GetFileAttributesW(wdest);\n+\n+\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n+\t\t/* Target exists; append last path part of source to target.\n+\t\t * Use FindFirstFile to parse the path */\n+\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n+\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n+\t\tFindClose(find_handle);\n+\n+\t\t/* Check the new target for existence */\n+\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n+\t}\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n+\t\tfs_copydir_helper(wsource, wdest);\n+\telse\n+\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+#ifdef CLAR_FIXTURE_PATH\n+\tfs_rm(fixture_path(_clar_path, \"*\"));\n+#endif\n+}\n+\n+#else\n+\n+#include <errno.h>\n+#include <string.h>\n+#include <limits.h>\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#if defined(__linux__)\n+# include <sys/sendfile.h>\n+#endif\n+\n+#if defined(__APPLE__)\n+# include <copyfile.h>\n+#endif\n+\n+static void basename_r(const char **out, int *out_len, const char *in)\n+{\n+\tsize_t in_len = strlen(in), start_pos;\n+\n+\tfor (in_len = strlen(in); in_len; in_len--) {\n+\t\tif (in[in_len - 1] != '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tfor (start_pos = in_len; start_pos; start_pos--) {\n+\t\tif (in[start_pos - 1] == '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tcl_assert(in_len - start_pos < INT_MAX);\n+\n+\tif (in_len - start_pos > 0) {\n+\t\t*out = &in[start_pos];\n+\t\t*out_len = (in_len - start_pos);\n+\t} else {\n+\t\t*out = \"/\";\n+\t\t*out_len = 1;\n+\t}\n+}\n+\n+static char *joinpath(const char *dir, const char *base, int base_len)\n+{\n+\tchar *out;\n+\tint len;\n+\n+\tif (base_len == -1) {\n+\t\tsize_t bl = strlen(base);\n+\n+\t\tcl_assert(bl < INT_MAX);\n+\t\tbase_len = (int)bl;\n+\t}\n+\n+\tlen = strlen(dir) + base_len + 2;\n+\tcl_assert(len > 0);\n+\n+\tcl_assert(out = malloc(len));\n+\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n+\n+\treturn out;\n+}\n+\n+static void\n+fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n+{\n+\tDIR *source_dir;\n+\tstruct dirent *d;\n+\n+\tmkdir(dest, dest_mode);\n+\n+\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n+\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(source, d->d_name, -1);\n+\t\tfs_copy(child, dest);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\n+\tclosedir(source_dir);\n+}\n+\n+static void\n+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n+{\n+\tint in, out;\n+\n+\tcl_must_pass((in = open(source, O_RDONLY)));\n+\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n+\n+#if USE_FCOPYFILE && defined(__APPLE__)\n+\t((void)(source_len)); /* unused */\n+\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n+#elif USE_SENDFILE && defined(__linux__)\n+\t{\n+\t\tssize_t ret = 0;\n+\n+\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n+\t\t\tsource_len -= (size_t)ret;\n+\t\t}\n+\t\tcl_assert(ret >= 0);\n+\t}\n+#else\n+\t{\n+\t\tchar buf[131072];\n+\t\tssize_t ret;\n+\n+\t\t((void)(source_len)); /* unused */\n+\n+\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n+\t\t\tsize_t len = (size_t)ret;\n+\n+\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n+\t\t\t\tcl_assert(ret <= (ssize_t)len);\n+\t\t\t\tlen -= ret;\n+\t\t\t}\n+\t\t\tcl_assert(ret >= 0);\n+\t\t}\n+\t\tcl_assert(ret == 0);\n+\t}\n+#endif\n+\n+\tclose(in);\n+\tclose(out);\n+}\n+\n+static void\n+fs_copy(const char *source, const char *_dest)\n+{\n+\tchar *dbuf = NULL;\n+\tconst char *dest = NULL;\n+\tstruct stat source_st, dest_st;\n+\n+\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n+\n+\tif (lstat(_dest, &dest_st) == 0) {\n+\t\tconst char *base;\n+\t\tint base_len;\n+\n+\t\t/* Target exists and is directory; append basename */\n+\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n+\n+\t\tbasename_r(&base, &base_len, source);\n+\t\tcl_assert(base_len < INT_MAX);\n+\n+\t\tdbuf = joinpath(_dest, base, base_len);\n+\t\tdest = dbuf;\n+\t} else if (errno != ENOENT) {\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t} else {\n+\t\tdest = _dest;\n+\t}\n+\n+\tif (S_ISDIR(source_st.st_mode)) {\n+\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n+\t} else {\n+\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n+\t}\n+\n+\tfree(dbuf);\n+}\n+\n+static void\n+fs_rmdir_helper(const char *path)\n+{\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n+\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(path, d->d_name, -1);\n+\t\tfs_rm(child);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\tclosedir(dir);\n+\n+\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n+}\n+\n+static void\n+fs_rm(const char *path)\n+{\n+\tstruct stat st;\n+\n+\tif (lstat(path, &st)) {\n+\t\tif (errno == ENOENT)\n+\t\t\treturn;\n+\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t}\n+\n+\tif (S_ISDIR(st.st_mode)) {\n+\t\tfs_rmdir_helper(path);\n+\t} else {\n+\t\tcl_must_pass(unlink(path));\n+\t}\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+\tclar_unsandbox();\n+\tclar_sandbox();\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\nnew file mode 100644\nindex 0000000000..c17e2f693b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/print.h\n@@ -0,0 +1,211 @@\n+/* clap: clar protocol, the traditional clar output format */\n+\n+static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n+\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n+}\n+\n+static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"\\n\\n\");\n+\tclar_report_all();\n+}\n+\n+static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tprintf(\"  %d) Failure:\\n\", num);\n+\n+\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n+\t\treport->suite,\n+\t\treport->test,\n+\t\terror->file,\n+\t\terror->line_number);\n+\n+\tprintf(\"  %s\\n\", error->error_msg);\n+\n+\tif (error->description != NULL)\n+\t\tprintf(\"  %s\\n\", error->description);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tif (_clar.verbosity > 1) {\n+\t\tprintf(\"%s::%s: \", suite_name, test_name);\n+\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n+\t\t}\n+\t} else {\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\".\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n+\t\t}\n+\n+\t\tfflush(stdout);\n+\t}\n+}\n+\n+static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tif (_clar.verbosity == 1)\n+\t\tprintf(\"\\n%s\", suite_name);\n+\n+\t(void)suite_index;\n+}\n+\n+static void clar_print_clap_onabort(const char *fmt, va_list arg)\n+{\n+\tvfprintf(stderr, fmt, arg);\n+}\n+\n+/* tap: test anywhere protocol format */\n+\n+static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)suite_names;\n+\tprintf(\"TAP version 13\\n\");\n+}\n+\n+static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"1..%d\\n\", test_count);\n+}\n+\n+static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\t(void)num;\n+\t(void)report;\n+\t(void)error;\n+}\n+\n+static void print_escaped(const char *str)\n+{\n+\tchar *c;\n+\n+\twhile ((c = strchr(str, '\\'')) != NULL) {\n+\t\tprintf(\"%.*s\", (int)(c - str), str);\n+\t\tprintf(\"''\");\n+\t\tstr = c + 1;\n+\t}\n+\n+\tprintf(\"%s\", str);\n+}\n+\n+static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tconst struct clar_error *error = _clar.last_report->errors;\n+\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tswitch(status) {\n+\tcase CL_TEST_OK:\n+\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\tcase CL_TEST_FAILURE:\n+\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\n+\t\tprintf(\"    ---\\n\");\n+\t\tprintf(\"    reason: |\\n\");\n+\t\tprintf(\"      %s\\n\", error->error_msg);\n+\n+\t\tif (error->description)\n+\t\t\tprintf(\"      %s\\n\", error->description);\n+\n+\t\tprintf(\"    at:\\n\");\n+\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n+\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n+\t\tprintf(\"      function: '%s'\\n\", error->function);\n+\t\tprintf(\"    ---\\n\");\n+\n+\t\tbreak;\n+\tcase CL_TEST_SKIP:\n+\tcase CL_TEST_NOTRUN:\n+\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\t}\n+\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n+}\n+\n+static void clar_print_tap_onabort(const char *fmt, va_list arg)\n+{\n+\tprintf(\"Bail out! \");\n+\tvprintf(fmt, arg);\n+\tfflush(stdout);\n+}\n+\n+/* indirection between protocol output selection */\n+\n+#define PRINT(FN, ...) do { \\\n+\t\tswitch (_clar.output_format) { \\\n+\t\t\tcase CL_OUTPUT_CLAP: \\\n+\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tcase CL_OUTPUT_TAP: \\\n+\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tdefault: \\\n+\t\t\t\tabort(); \\\n+\t\t} \\\n+\t} while (0)\n+\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\tPRINT(init, test_count, suite_count, suite_names);\n+}\n+\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\tPRINT(shutdown, test_count, suite_count, error_count);\n+}\n+\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tPRINT(error, num, report, error);\n+}\n+\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tPRINT(ontest, suite_name, test_name, test_number, status);\n+}\n+\n+static void clar_print_onsuite(const char *suite_name, int suite_index)\n+{\n+\tPRINT(onsuite, suite_name, suite_index);\n+}\n+\n+static void clar_print_onabort(const char *msg, ...)\n+{\n+\tva_list argp;\n+\tva_start(argp, msg);\n+\tPRINT(onabort, msg, argp);\n+\tva_end(argp);\n+}\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nnew file mode 100644\nindex 0000000000..7c177f3525\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -0,0 +1,153 @@\n+#ifdef __APPLE__\n+#include <sys/syslimits.h>\n+#endif\n+\n+static char _clar_path[4096 + 1];\n+\n+static int\n+is_valid_tmp_path(const char *path)\n+{\n+\tSTAT_T st;\n+\n+\tif (stat(path, &st) != 0)\n+\t\treturn 0;\n+\n+\tif (!S_ISDIR(st.st_mode))\n+\t\treturn 0;\n+\n+\treturn (access(path, W_OK) == 0);\n+}\n+\n+static int\n+find_tmp_path(char *buffer, size_t length)\n+{\n+#ifndef _WIN32\n+\tstatic const size_t var_count = 5;\n+\tstatic const char *env_vars[] = {\n+\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n+\t};\n+\n+\tsize_t i;\n+\n+\tfor (i = 0; i < var_count; ++i) {\n+\t\tconst char *env = getenv(env_vars[i]);\n+\t\tif (!env)\n+\t\t\tcontinue;\n+\n+\t\tif (is_valid_tmp_path(env)) {\n+#ifdef __APPLE__\n+\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n+\t\t\t\treturn 0;\n+#endif\n+\t\t\tstrncpy(buffer, env, length - 1);\n+\t\t\tbuffer[length - 1] = '\\0';\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\t/* If the environment doesn't say anything, try to use /tmp */\n+\tif (is_valid_tmp_path(\"/tmp\")) {\n+#ifdef __APPLE__\n+\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n+\t\t\treturn 0;\n+#endif\n+\t\tstrncpy(buffer, \"/tmp\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+#else\n+\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n+\tif (env_len > 0 && env_len < (DWORD)length)\n+\t\treturn 0;\n+\n+\tif (GetTempPath((DWORD)length, buffer))\n+\t\treturn 0;\n+#endif\n+\n+\t/* This system doesn't like us, try to use the current directory */\n+\tif (is_valid_tmp_path(\".\")) {\n+\t\tstrncpy(buffer, \".\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static void clar_unsandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0')\n+\t\treturn;\n+\n+\tcl_must_pass(chdir(\"..\"));\n+\n+\tfs_rm(_clar_path);\n+}\n+\n+static int build_sandbox_path(void)\n+{\n+#ifdef CLAR_TMPDIR\n+\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n+#else\n+\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n+#endif\n+\n+\tsize_t len;\n+\n+\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n+\t\treturn -1;\n+\n+\tlen = strlen(_clar_path);\n+\n+#ifdef _WIN32\n+\t{ /* normalize path to POSIX forward slashes */\n+\t\tsize_t i;\n+\t\tfor (i = 0; i < len; ++i) {\n+\t\t\tif (_clar_path[i] == '\\\\')\n+\t\t\t\t_clar_path[i] = '/';\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (_clar_path[len - 1] != '/') {\n+\t\t_clar_path[len++] = '/';\n+\t}\n+\n+\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n+\n+#if defined(__MINGW32__)\n+\tif (_mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(_WIN32)\n+\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#else\n+\tif (mkdtemp(_clar_path) == NULL)\n+\t\treturn -1;\n+#endif\n+\n+\treturn 0;\n+}\n+\n+static int clar_sandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n+\t\treturn -1;\n+\n+\tif (chdir(_clar_path) != 0)\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const char *clar_sandbox_path(void)\n+{\n+\treturn _clar_path;\n+}\ndiff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\nnew file mode 100644\nindex 0000000000..4dd352e28b\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/summary.h\n@@ -0,0 +1,143 @@\n+\n+#include <stdio.h>\n+#include <time.h>\n+\n+static int clar_summary_close_tag(\n+    struct clar_summary *summary, const char *tag, int indent)\n+{\n+\tconst char *indt;\n+\n+\tif (indent == 0) indt = \"\";\n+\telse if (indent == 1) indt = \"\\t\";\n+\telse indt = \"\\t\\t\";\n+\n+\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n+}\n+\n+static int clar_summary_testsuites(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n+}\n+\n+static int clar_summary_testsuite(struct clar_summary *summary,\n+    int idn, const char *name, time_t timestamp,\n+    int test_count, int fail_count, int error_count)\n+{\n+\tstruct tm *tm = localtime(&timestamp);\n+\tchar iso_dt[20];\n+\n+\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n+\t\treturn -1;\n+\n+\treturn fprintf(summary->fp, \"\\t<testsuite\"\n+\t\t       \" id=\\\"%d\\\"\"\n+\t\t       \" name=\\\"%s\\\"\"\n+\t\t       \" hostname=\\\"localhost\\\"\"\n+\t\t       \" timestamp=\\\"%s\\\"\"\n+\t\t       \" tests=\\\"%d\\\"\"\n+\t\t       \" failures=\\\"%d\\\"\"\n+\t\t       \" errors=\\\"%d\\\">\\n\",\n+\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n+}\n+\n+static int clar_summary_testcase(struct clar_summary *summary,\n+    const char *name, const char *classname, double elapsed)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n+\t\tname, classname, elapsed);\n+}\n+\n+static int clar_summary_failure(struct clar_summary *summary,\n+    const char *type, const char *message, const char *desc)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n+\t    type, message, desc);\n+}\n+\n+static int clar_summary_skipped(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n+}\n+\n+struct clar_summary *clar_summary_init(const char *filename)\n+{\n+\tstruct clar_summary *summary;\n+\tFILE *fp;\n+\n+\tif ((fp = fopen(filename, \"w\")) == NULL) {\n+\t\tperror(\"fopen\");\n+\t\treturn NULL;\n+\t}\n+\n+\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n+\t\tperror(\"malloc\");\n+\t\tfclose(fp);\n+\t\treturn NULL;\n+\t}\n+\n+\tsummary->filename = filename;\n+\tsummary->fp = fp;\n+\n+\treturn summary;\n+}\n+\n+int clar_summary_shutdown(struct clar_summary *summary)\n+{\n+\tstruct clar_report *report;\n+\tconst char *last_suite = NULL;\n+\n+\tif (clar_summary_testsuites(summary) < 0)\n+\t\tgoto on_error;\n+\n+\treport = _clar.reports;\n+\twhile (report != NULL) {\n+\t\tstruct clar_error *error = report->errors;\n+\n+\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n+\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\n+\t\tlast_suite = report->suite;\n+\n+\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n+\n+\t\twhile (error != NULL) {\n+\t\t\tif (clar_summary_failure(summary, \"assert\",\n+\t\t\t    error->error_msg, error->description) < 0)\n+\t\t\t\tgoto on_error;\n+\n+\t\t\terror = error->next;\n+\t\t}\n+\n+\t\tif (report->status == CL_TEST_SKIP)\n+\t\t\tclar_summary_skipped(summary);\n+\n+\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n+\t\t\tgoto on_error;\n+\n+\t\treport = report->next;\n+\n+\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\t}\n+\n+\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n+\t    fclose(summary->fp) != 0)\n+\t\tgoto on_error;\n+\n+\tprintf(\"written summary file to %s\\n\", summary->filename);\n+\n+\tfree(summary);\n+\treturn 0;\n+\n+on_error:\n+\tfclose(summary->fp);\n+\tfree(summary);\n+\treturn -1;\n+}\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nnew file mode 100755\nindex 0000000000..80996ac3e7\n--- /dev/null\n+++ b/t/unit-tests/clar/generate.py\n@@ -0,0 +1,266 @@\n+#!/usr/bin/env python\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+from __future__ import with_statement\n+from string import Template\n+import re, fnmatch, os, sys, codecs, pickle\n+\n+class Module(object):\n+    class Template(object):\n+        def __init__(self, module):\n+            self.module = module\n+\n+        def _render_callback(self, cb):\n+            if not cb:\n+                return '    { NULL, NULL }'\n+            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n+\n+    class DeclarationTemplate(Template):\n+        def render(self):\n+            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n+\n+            for initializer in self.module.initializers:\n+                out += \"extern %s;\\n\" % initializer['declaration']\n+\n+            if self.module.cleanup:\n+                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n+\n+            return out\n+\n+    class CallbacksTemplate(Template):\n+        def render(self):\n+            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n+            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n+            out += \"\\n};\\n\"\n+            return out\n+\n+    class InfoTemplate(Template):\n+        def render(self):\n+            templates = []\n+\n+            initializers = self.module.initializers\n+            if len(initializers) == 0:\n+                initializers = [ None ]\n+\n+            for initializer in initializers:\n+                name = self.module.clean_name()\n+                if initializer and initializer['short_name'].startswith('initialize_'):\n+                    variant = initializer['short_name'][len('initialize_'):]\n+                    name += \" (%s)\" % variant.replace('_', ' ')\n+\n+                template = Template(\n+            r\"\"\"\n+    {\n+        \"${clean_name}\",\n+    ${initialize},\n+    ${cleanup},\n+        ${cb_ptr}, ${cb_count}, ${enabled}\n+    }\"\"\"\n+                ).substitute(\n+                    clean_name = name,\n+                    initialize = self._render_callback(initializer),\n+                    cleanup = self._render_callback(self.module.cleanup),\n+                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n+                    cb_count = len(self.module.callbacks),\n+                    enabled = int(self.module.enabled)\n+                )\n+                templates.append(template)\n+\n+            return ','.join(templates)\n+\n+    def __init__(self, name):\n+        self.name = name\n+\n+        self.mtime = None\n+        self.enabled = True\n+        self.modified = False\n+\n+    def clean_name(self):\n+        return self.name.replace(\"_\", \"::\")\n+\n+    def _skip_comments(self, text):\n+        SKIP_COMMENTS_REGEX = re.compile(\n+            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n+            re.DOTALL | re.MULTILINE)\n+\n+        def _replacer(match):\n+            s = match.group(0)\n+            return \"\" if s.startswith('/') else s\n+\n+        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n+\n+    def parse(self, contents):\n+        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n+\n+        contents = self._skip_comments(contents)\n+        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n+\n+        self.callbacks = []\n+        self.initializers = []\n+        self.cleanup = None\n+\n+        for (declaration, symbol, short_name) in regex.findall(contents):\n+            data = {\n+                \"short_name\" : short_name,\n+                \"declaration\" : declaration,\n+                \"symbol\" : symbol\n+            }\n+\n+            if short_name.startswith('initialize'):\n+                self.initializers.append(data)\n+            elif short_name == 'cleanup':\n+                self.cleanup = data\n+            else:\n+                self.callbacks.append(data)\n+\n+        return self.callbacks != []\n+\n+    def refresh(self, path):\n+        self.modified = False\n+\n+        try:\n+            st = os.stat(path)\n+\n+            # Not modified\n+            if st.st_mtime == self.mtime:\n+                return True\n+\n+            self.modified = True\n+            self.mtime = st.st_mtime\n+\n+            with codecs.open(path, encoding='utf-8') as fp:\n+                raw_content = fp.read()\n+\n+        except IOError:\n+            return False\n+\n+        return self.parse(raw_content)\n+\n+class TestSuite(object):\n+\n+    def __init__(self, path, output):\n+        self.path = path\n+        self.output = output\n+\n+    def should_generate(self, path):\n+        if not os.path.isfile(path):\n+            return True\n+\n+        if any(module.modified for module in self.modules.values()):\n+            return True\n+\n+        return False\n+\n+    def find_modules(self):\n+        modules = []\n+        for root, _, files in os.walk(self.path):\n+            module_root = root[len(self.path):]\n+            module_root = [c for c in module_root.split(os.sep) if c]\n+\n+            tests_in_module = fnmatch.filter(files, \"*.c\")\n+\n+            for test_file in tests_in_module:\n+                full_path = os.path.join(root, test_file)\n+                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n+\n+                modules.append((full_path, module_name))\n+\n+        return modules\n+\n+    def load_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        cache = {}\n+\n+        try:\n+            fp = open(path, 'rb')\n+            cache = pickle.load(fp)\n+            fp.close()\n+        except (IOError, ValueError):\n+            pass\n+\n+        return cache\n+\n+    def save_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        with open(path, 'wb') as cache:\n+            pickle.dump(self.modules, cache)\n+\n+    def load(self, force = False):\n+        module_data = self.find_modules()\n+        self.modules = {} if force else self.load_cache()\n+\n+        for path, name in module_data:\n+            if name not in self.modules:\n+                self.modules[name] = Module(name)\n+\n+            if not self.modules[name].refresh(path):\n+                del self.modules[name]\n+\n+    def disable(self, excluded):\n+        for exclude in excluded:\n+            for module in self.modules.values():\n+                name = module.clean_name()\n+                if name.startswith(exclude):\n+                    module.enabled = False\n+                    module.modified = True\n+\n+    def suite_count(self):\n+        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n+\n+    def callback_count(self):\n+        return sum(len(module.callbacks) for module in self.modules.values())\n+\n+    def write(self):\n+        output = os.path.join(self.output, 'clar.suite')\n+\n+        if not self.should_generate(output):\n+            return False\n+\n+        with open(output, 'w') as data:\n+            modules = sorted(self.modules.values(), key=lambda module: module.name)\n+\n+            for module in modules:\n+                t = Module.DeclarationTemplate(module)\n+                data.write(t.render())\n+\n+            for module in modules:\n+                t = Module.CallbacksTemplate(module)\n+                data.write(t.render())\n+\n+            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n+                Module.InfoTemplate(module).render() for module in modules\n+            ) + \"\\n};\\n\"\n+\n+            data.write(suites)\n+\n+            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n+            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n+\n+        self.save_cache()\n+        return True\n+\n+if __name__ == '__main__':\n+    from optparse import OptionParser\n+\n+    parser = OptionParser()\n+    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n+    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n+    parser.add_option('-o', '--output', dest='output')\n+\n+    options, args = parser.parse_args()\n+    if len(args) > 1:\n+        print(\"More than one path given\")\n+        sys.exit(1)\n+\n+    path = args.pop() if args else '.'\n+    output = options.output or path\n+    suite = TestSuite(path, output)\n+    suite.load(options.force)\n+    suite.disable(options.excluded)\n+    if suite.write():\n+        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nnew file mode 100644\nindex 0000000000..a477d0c40c\n--- /dev/null\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -0,0 +1,4 @@\n+clar.suite\n+.clarcache\n+clar_test\n+*.o\ndiff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\nnew file mode 100644\nindex 0000000000..93c6b2ad32\n--- /dev/null\n+++ b/t/unit-tests/clar/test/Makefile\n@@ -0,0 +1,39 @@\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+#\n+# Set up the path to the clar sources and to the fixtures directory\n+#\n+# The fixture path needs to be an absolute path so it can be used\n+# even after we have chdir'ed into the test directory while testing.\n+#\n+CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n+TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n+CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n+CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n+\n+CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n+\n+.PHONY: clean\n+\n+# list the objects that go into our test\n+objects = main.o sample.o\n+\n+# build the test executable itself\n+clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n+\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n+\n+# test object files depend on clar macros\n+$(objects) : $(CLAR_PATH)clar.h\n+\n+# build the clar.suite file of test metadata\n+clar.suite:\n+\tpython \"$(CLAR_PATH)generate.py\" .\n+\n+# remove all generated files\n+clean:\n+\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\ndiff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\nnew file mode 100644\nindex 0000000000..0fcaa639aa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/clar_test.h\n@@ -0,0 +1,16 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST__\n+#define __CLAR_TEST__\n+\n+/* Import the standard clar helper functions */\n+#include \"clar.h\"\n+\n+/* Your custom shared includes / defines here */\n+extern int global_test_counter;\n+\n+#endif\ndiff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\nnew file mode 100644\nindex 0000000000..59e56ad255\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c\n@@ -0,0 +1,40 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Sample main() for clar tests.\n+ *\n+ * You should write your own main routine for clar tests that does specific\n+ * setup and teardown as necessary for your application.  The only required\n+ * line is the call to `clar_test(argc, argv)`, which will execute the test\n+ * suite.  If you want to check the return value of the test application,\n+ * your main() should return the same value returned by clar_test().\n+ */\n+\n+int global_test_counter = 0;\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\tint ret;\n+\n+\t/* Your custom initialization here */\n+\tglobal_test_counter = 0;\n+\n+\t/* Run the test suite */\n+\tret = clar_test(argc, argv);\n+\n+\t/* Your custom cleanup here */\n+\tcl_assert_equal_i(8, global_test_counter);\n+\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\nnew file mode 100644\nindex 0000000000..a4d91b72fa\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c.sample\n@@ -0,0 +1,27 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Minimal main() for clar tests.\n+ *\n+ * Modify this with any application specific setup or teardown that you need.\n+ * The only required line is the call to `clar_test(argc, argv)`, which will\n+ * execute the test suite.  If you want to check the return value of the test\n+ * application, main() should return the same value returned by clar_test().\n+ */\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\t/* Run the test suite */\n+\treturn clar_test(argc, argv);\n+}\ndiff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\nnew file mode 100644\nindex 0000000000..220f4aa98a\n--- /dev/null\n+++ b/t/unit-tests/clar/test/resources/test/file\n@@ -0,0 +1 @@\n+File\ndiff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\nnew file mode 100644\nindex 0000000000..faa1209262\n--- /dev/null\n+++ b/t/unit-tests/clar/test/sample.c\n@@ -0,0 +1,84 @@\n+#include \"clar_test.h\"\n+#include <sys/stat.h>\n+\n+static int file_size(const char *filename)\n+{\n+\tstruct stat st;\n+\n+\tif (stat(filename, &st) == 0)\n+\t\treturn (int)st.st_size;\n+\treturn -1;\n+}\n+\n+void test_sample__initialize(void)\n+{\n+\tglobal_test_counter++;\n+}\n+\n+void test_sample__cleanup(void)\n+{\n+\tcl_fixture_cleanup(\"test\");\n+\n+\tcl_assert(file_size(\"test/file\") == -1);\n+}\n+\n+void test_sample__1(void)\n+{\n+\tcl_assert(1);\n+\tcl_must_pass(0);  /* 0 == success */\n+\tcl_must_fail(-1); /* <0 == failure */\n+\tcl_must_pass(-1); /* demonstrate a failing call */\n+}\n+\n+void test_sample__2(void)\n+{\n+\tcl_fixture_sandbox(\"test\");\n+\n+\tcl_assert(file_size(\"test/nonexistent\") == -1);\n+\tcl_assert(file_size(\"test/file\") > 0);\n+\tcl_assert(100 == 101);\n+}\n+\n+void test_sample__strings(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_s(\"expected\", actual);\n+\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n+\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n+}\n+\n+void test_sample__strings_with_length(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_strn(\"expected_\", actual, 8);\n+\tcl_assert_equal_strn(\"exactly\", actual, 2);\n+\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n+\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n+}\n+\n+void test_sample__int(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i(100, value);\n+\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n+}\n+\n+void test_sample__int_fmt(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n+}\n+\n+void test_sample__bool(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n+\tcl_assert_equal_b(0, value);\n+}\n+\n+void test_sample__ptr(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n+\tcl_assert_equal_p(&actual, actual);\n+}\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501094","messageId":"88ea94ce16c365f4b6b634a274632db134c9eba1.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 6/9] Makefile: do not use sparse on third-party sources","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:53Z","receivedAt":"2024-08-16T07:36:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have several third-party sources in our codebase that we have\nimported from upstream projects. These sources are mostly excluded from\nour static analysis, for example when running Coccinelle.\n\nDo the same for our \"sparse\" target by filtering them out.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 5384bf8104..377ea7e144 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3251,7 +3251,8 @@ t/helper/test-%$X: t/helper/test-%.o GIT-LDFLAGS $(GITLIBS) $(REFTABLE_TEST_LIB)\n check-sha1:: t/helper/test-tool$X\n \tt/helper/test-sha1.sh\n \n-SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n+SP_SRC = $(filter-out $(THIRD_PARTY_SOURCES),$(patsubst %.o,%.c,$(OBJECTS)))\n+SP_OBJ = $(patsubst %.c,%.sp,$(SP_SRC))\n \n $(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501095","messageId":"05bcb5bef6c519f6955d046240ae729eeaae1726.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 7/9] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:56Z","receivedAt":"2024-08-16T07:36:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Wire up the clar unit testing framework by introducing a new\n\"unit-tests\" executable. In contrast to the existing framework, this\nwill result in a single executable for all test suites. The ability to\npick specific tests to execute is retained via functionality built into\nthe clar itself.\n\nNote that we need to be a bit careful about how we need to invalidate\nour Makefile rules. While we obviously have to regenerate the clar suite\nwhen our test suites change, we also have to invalidate it in case any\nof the test suites gets removed. We do so by using our typical pattern\nof creating a `GIT-TEST-SUITES` file that gets updated whenever the set\nof test suites changes, so that we can easily depend on that file.\n\nAnother specialty is that we generate a \"clar-decls.h\" file. The test\nfunctions are neither static, nor do they have external declarations.\nThis is because they are getting parsed via \"generate.py\", which then\ncreates the external generations that get populated into an array. These\ndeclarations are only seen by the main function though.\n\nThe consequence is that we will get a bunch of \"missing prototypes\"\nerrors from our compiler for each of these test functions. To fix those\nerrors, we extract the `extern` declarations from \"clar.suite\" and put\nthem into a standalone header that then gets included by each of our\nunit tests. This gets rid of compiler warnings for every function which\nhas been extracted by \"generate.py\". More importantly though, it does\n_not_ get rid of warnings in case a function really isn't being used by\nanything. Thus, it would cause a compiler error if a function name was\nmistyped and thus not picked up by \"generate.py\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Makefile                       | 36 +++++++++++++++++++++---\n t/Makefile                     |  1 +\n t/unit-tests/.gitignore        |  2 ++\n t/unit-tests/clar-generate.awk | 50 ++++++++++++++++++++++++++++++++++\n t/unit-tests/unit-test.c       | 18 ++++++++++++\n t/unit-tests/unit-test.h       |  3 ++\n 7 files changed, 107 insertions(+), 4 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\ndiff --git a/.gitignore b/.gitignore\nindex 8caf3700c2..6687bd6db4 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,6 +9,7 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n+/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex 377ea7e144..e6206ca8c7 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -914,6 +914,8 @@ REFTABLE_TEST_LIB = reftable/libreftable_test.a\n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n GENERATED_H += hook-list.h\n+GENERATED_H += $(UNIT_TEST_DIR)/clar-decls.h\n+GENERATED_H += $(UNIT_TEST_DIR)/clar.suite\n \n .PHONY: generated-hdrs\n generated-hdrs: $(GENERATED_H)\n@@ -1334,6 +1336,11 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n+UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n+\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n@@ -2713,6 +2720,7 @@ OBJECTS += $(XDIFF_OBJS)\n OBJECTS += $(FUZZ_OBJS)\n OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n OBJECTS += $(UNIT_TEST_OBJS)\n+OBJECTS += $(UNIT_TESTS_OBJS)\n \n ifndef NO_CURL\n \tOBJECTS += http.o http-walker.o remote-curl.o\n@@ -3215,7 +3223,7 @@ endif\n \n test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))\n \n-all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)\n+all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n \n bin-wrappers/%: wrap-for-bin.sh\n \t$(call mkdir_p_parent_template)\n@@ -3647,7 +3655,7 @@ endif\n \n artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n \t\tGIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \\\n-\t\t$(UNIT_TEST_PROGS) $(MOFILES)\n+\t\t$(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) $(MOFILES)\n \t$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \\\n \t\tSHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'\n \ttest -n \"$(ARTIFACTS_DIRECTORY)\"\n@@ -3703,6 +3711,7 @@ cocciclean:\n \n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n+\t$(RM) GIT-TEST-SUITES\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3862,7 +3871,26 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n \n+GIT-TEST-SUITES: FORCE\n+\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n+\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n+\t\techo >&2 \"    * new test suites\"; \\\n+\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n+            fi\n+\n+$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n+\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n+\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n+\tdone >$@\n+$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n+\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n+$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR)\n+$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n+\t$(call mkdir_p_parent_template)\n+\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n+\n .PHONY: build-unit-tests unit-tests\n-build-unit-tests: $(UNIT_TEST_PROGS)\n-unit-tests: $(UNIT_TEST_PROGS) t/helper/test-tool$X\n+build-unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n+unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) t/helper/test-tool$X\n \t$(MAKE) -C t/ unit-tests\ndiff --git a/t/Makefile b/t/Makefile\nindex d2212de0b7..131ffd778f 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -48,6 +48,7 @@ CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.tes\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n+UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n UNIT_TESTS = $(sort $(UNIT_TEST_PROGRAMS))\n UNIT_TESTS_NO_DIR = $(notdir $(UNIT_TESTS))\n \ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex 5e56e040ec..d0632ec7f9 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1 +1,3 @@\n /bin\n+/clar.suite\n+/clar-decls.h\ndiff --git a/t/unit-tests/clar-generate.awk b/t/unit-tests/clar-generate.awk\nnew file mode 100644\nindex 0000000000..ab71ce6c9f\n--- /dev/null\n+++ b/t/unit-tests/clar-generate.awk\n@@ -0,0 +1,50 @@\n+function add_suite(suite, initialize, cleanup, count) {\n+       if (!suite) return\n+       suite_count++\n+       callback_count += count\n+       suites = suites \"    {\\n\"\n+       suites = suites \"        \\\"\" suite \"\\\",\\n\"\n+       suites = suites \"        \" initialize \",\\n\"\n+       suites = suites \"        \" cleanup \",\\n\"\n+       suites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n+       suites = suites \"    },\\n\"\n+}\n+\n+BEGIN {\n+       suites = \"static struct clar_suite _clar_suites[] = {\\n\"\n+}\n+\n+{\n+       print\n+       name = $3; sub(/\\(.*$/, \"\", name)\n+       suite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n+       short_name = name; sub(/^.*__/, \"\", short_name)\n+       cb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n+       if (suite != prev_suite) {\n+               add_suite(prev_suite, initialize, cleanup, count)\n+               if (callbacks) callbacks = callbacks \"};\\n\"\n+               callbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n+               initialize = \"{ NULL, NULL }\"\n+               cleanup = \"{ NULL, NULL }\"\n+               count = 0\n+               prev_suite = suite\n+       }\n+       if (short_name == \"initialize\") {\n+               initialize = cb\n+       } else if (short_name == \"cleanup\") {\n+               cleanup = cb\n+       } else {\n+               callbacks = callbacks \"    \" cb \",\\n\"\n+               count++\n+       }\n+}\n+\n+END {\n+       add_suite(suite, initialize, cleanup, count)\n+       suites = suites \"};\"\n+       if (callbacks) callbacks = callbacks \"};\"\n+       print callbacks\n+       print suites\n+       print \"static const size_t _clar_suite_count = \" suite_count \";\"\n+       print \"static const size_t _clar_callback_count = \" callback_count \";\"\n+}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nnew file mode 100644\nindex 0000000000..32a81299e9\n--- /dev/null\n+++ b/t/unit-tests/unit-test.c\n@@ -0,0 +1,18 @@\n+#include \"unit-test.h\"\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tconst char **argv_copy;\n+\tint ret;\n+\n+\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n+\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tCOPY_ARRAY(argv_copy, argv, argc);\n+\targv_copy[argc++] = \"-t\";\n+\targv_copy[argc] = NULL;\n+\n+\tret = clar_test(argc, (char **) argv_copy);\n+\n+\tfree(argv_copy);\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nnew file mode 100644\nindex 0000000000..66ec2387cc\n--- /dev/null\n+++ b/t/unit-tests/unit-test.h\n@@ -0,0 +1,3 @@\n+#include \"git-compat-util.h\"\n+#include \"clar/clar.h\"\n+#include \"clar-decls.h\"\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501096","messageId":"db53673294e6b1aee7e095eb57b571b9e17e5757.1723791831.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"[PATCH v5 3/9] t/clar: fix compatibility with NonStop","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-16T07:04:42Z","receivedAt":"2024-08-16T07:36:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The NonStop platform does not have `mkdtemp()` available, which we rely\non in `build_sandbox_path()`. Fix this issue by using `mktemp()` and\n`mkdir()` instead on this platform.\n\nThis has been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/96\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 7c177f3525..e25057b7c4 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -120,6 +120,12 @@ static int build_sandbox_path(void)\n \tif (_mktemp(_clar_path) == NULL)\n \t\treturn -1;\n \n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(__TANDEM)\n+\tif (mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n \tif (mkdir(_clar_path, 0700) != 0)\n \t\treturn -1;\n #elif defined(_WIN32)\n-- \n2.46.0.46.g406f326d27.dirty\n\n"},{"id":"501137","messageId":"b87700d2-0c9a-4d0c-9ee4-e6a91278d596@gmail.com","threadId":"61874","inReplyTo":"cover.1723791831.git.ps@pks.im","subject":"Re: [PATCH v5 0/9] Introduce clar testing framework","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-16T13:37:34Z","receivedAt":"2024-08-16T13:37:39Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 16/08/2024 08:04, Patrick Steinhardt wrote:\n> Hi,\n> \n> this is the fifth version of my patch series that introduces the clar\n> testing framework for our unit tests.\n\nThanks for working on this, I'm broadly in favor of this change. I\nlike the way it keeps each test as a function and adds automatic test\nregistration with support for setup and teardown functions. I am keen\nthough to keep an emphasis on good diagnostic messages when tests\nfail. Looking at the conversions in this series all of the test_msg()\nlines that provide useful debugging context are removed. I'm not sure\nusing yaml to report errors rather than human readable messages is an\nimprovement either.\n\nI wonder if we want to either improve the assertions offered by clar\nor write our own. I find the names of the cl_assert_equal_?()\nfunctions are a bit cumbersome. The aim of the check_* names was to\ntry and be both concise and descriptive. Adding our own check_* macros\non top of clar would also make it easier to port our existing tests.\n\nHere are some thought having read through the assertion and error\nreporting code:\n\n  - As I think you've pointed out elsewhere there are no equivalents\n    for check_int(a, <|<=|>|>=, b) so we're forced to use cl_assert()\n    and forego the better diagnostic messages that come from a\n    dedicated comparison macro. We should fix this as a priority.\n\n  - cl_assert_equal_i() casts its arguments to int whereas check_int()\n    and check_uint() are careful to avoid truncation and keep the\n    original signedness (if that's a word). I think that's unlikely to\n    be a problem with our current test but could trip us up in the\n    future.\n\n  - cl_assert_equal_s() prints each argument as-is. This means\n    that it passes NULL arguments through to snprintf() which is\n    undefined according to the C standard. Compare this to check_str()\n    that is NULL safe and is careful to escape control characters and\n    add delimiters to the beginning and end of the string to make it\n    obvious when a string contains leading or trailing whitespace.\n\n  - The cl_assert_equal_?() macros lack type safety for the arguments\n    being compared as they are wrappers around a variadic function.\n    That could be fixed by having each macros wrap a dedicated\n    function that wraps clar__fail().\n\n  - There is no equivalent of test_todo() to mark assertions that are\n    expected to fail. We're not using that yet in our tests but our\n    experience with the integration tests suggests that we are likely\n    to want this in the future.\n\n  - To me the \"sandbox\" feature is mis-named as it does not provide any\n    confinement. It is instead a useful mechanism for running a test in\n    a temporary directory created from a template.\n\n  - There are no checks for failing memory allocations - the return\n    value of calloc() and strdup() are used without checking for NULL.\n\n  - The use of longjmp is a bit of a double edged sword as it makes it\n    easy to leak resources on test failures.\n\nBest Wishes\n\nPhillip\n\n> Changes compared to v4:\n> \n>    - The whitespace fixes have been merged upstream, so I've updated the\n>      embedded copy of clar and dropped the subsequent patch that fixed\n>      them in our copy. The NonStop compatibility fixes have not yet been\n>      merged as the pull request needs some more work.\n> \n>    - Both \"clar-decls.h\" and \"clar.suite\" are now part of GENERATED_H.\n>      This brings removal of these files via \"make clean\" for free.\n> \n>    - The \"sparse\" target already depends on GENERATED_H, but in a broken\n>      way. I've fixed that in a new commit.\n> \n>    - The \"sparse\" target no longer checks external sources, including the\n>      clar sources.\n> \n>    - The \"hdr-check\" target now depends on GENERATED_H, as well. This\n>      avoids having to manually wire up dependencies on generated headers\n>      per file, which seems rather unmaintainable to me.\n> \n> With this, the \"hdr-check\" and \"sparse\" targets all work on my machine\n> now.\n> \n> Thanks!\n> \n> Patrick\n> \n> Patrick Steinhardt (9):\n>    t: do not pass GIT_TEST_OPTS to unit tests with prove\n>    t: import the clar unit testing framework\n>    t/clar: fix compatibility with NonStop\n>    Makefile: fix sparse dependency on GENERATED_H\n>    Makefile: make hdr-check depend on generated headers\n>    Makefile: do not use sparse on third-party sources\n>    Makefile: wire up the clar unit testing framework\n>    t/unit-tests: convert strvec tests to use clar\n>    t/unit-tests: convert ctype tests to use clar\n> \n>   .gitignore                                 |   1 +\n>   Documentation/technical/unit-tests.txt     |   2 +\n>   Makefile                                   |  53 +-\n>   t/Makefile                                 |   4 +-\n>   t/run-test.sh                              |   2 +-\n>   t/unit-tests/.gitignore                    |   2 +\n>   t/unit-tests/clar-generate.awk             |  50 ++\n>   t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n>   t/unit-tests/clar/COPYING                  |  15 +\n>   t/unit-tests/clar/README.md                | 329 ++++++++\n>   t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n>   t/unit-tests/clar/clar.h                   | 173 +++++\n>   t/unit-tests/clar/clar/fixtures.h          |  50 ++\n>   t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n>   t/unit-tests/clar/clar/print.h             | 211 ++++++\n>   t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n>   t/unit-tests/clar/clar/summary.h           | 143 ++++\n>   t/unit-tests/clar/generate.py              | 266 +++++++\n>   t/unit-tests/clar/test/.gitignore          |   4 +\n>   t/unit-tests/clar/test/Makefile            |  39 +\n>   t/unit-tests/clar/test/clar_test.h         |  16 +\n>   t/unit-tests/clar/test/main.c              |  40 +\n>   t/unit-tests/clar/test/main.c.sample       |  27 +\n>   t/unit-tests/clar/test/resources/test/file |   1 +\n>   t/unit-tests/clar/test/sample.c            |  84 ++\n>   t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n>   t/unit-tests/{t-strvec.c => strvec.c}      | 119 ++-\n>   t/unit-tests/unit-test.c                   |  17 +\n>   t/unit-tests/unit-test.h                   |   3 +\n>   29 files changed, 3166 insertions(+), 102 deletions(-)\n>   create mode 100644 t/unit-tests/clar-generate.awk\n>   create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n>   create mode 100644 t/unit-tests/clar/COPYING\n>   create mode 100644 t/unit-tests/clar/README.md\n>   create mode 100644 t/unit-tests/clar/clar.c\n>   create mode 100644 t/unit-tests/clar/clar.h\n>   create mode 100644 t/unit-tests/clar/clar/fixtures.h\n>   create mode 100644 t/unit-tests/clar/clar/fs.h\n>   create mode 100644 t/unit-tests/clar/clar/print.h\n>   create mode 100644 t/unit-tests/clar/clar/sandbox.h\n>   create mode 100644 t/unit-tests/clar/clar/summary.h\n>   create mode 100755 t/unit-tests/clar/generate.py\n>   create mode 100644 t/unit-tests/clar/test/.gitignore\n>   create mode 100644 t/unit-tests/clar/test/Makefile\n>   create mode 100644 t/unit-tests/clar/test/clar_test.h\n>   create mode 100644 t/unit-tests/clar/test/main.c\n>   create mode 100644 t/unit-tests/clar/test/main.c.sample\n>   create mode 100644 t/unit-tests/clar/test/resources/test/file\n>   create mode 100644 t/unit-tests/clar/test/sample.c\n>   rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n>   rename t/unit-tests/{t-strvec.c => strvec.c} (54%)\n>   create mode 100644 t/unit-tests/unit-test.c\n>   create mode 100644 t/unit-tests/unit-test.h\n> \n> Range-diff against v4:\n>   1:  086dd728a7 !  1:  832dc0496f t: do not pass GIT_TEST_OPTS to unit tests with prove\n>      @@ Commit message\n>           environment variable. Like this, we can conditionally forward it to our\n>           test scripts, only.\n>       \n>      +    Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>      +\n>        ## t/Makefile ##\n>       @@ t/Makefile: failed:\n>        \ttest -z \"$$failed\" || $(MAKE) $$failed\n>   2:  5c22e0b3b9 !  2:  3690607933 t: import the clar unit testing framework\n>      @@ Metadata\n>        ## Commit message ##\n>           t: import the clar unit testing framework\n>       \n>      -    Import the clar unit testing framework at commit faa8419 (Merge pull\n>      -    request #93 from clar-test/ethomson/fixtures, 2023-12-14). The framework\n>      +    Import the clar unit testing framework at commit 1516124 (Merge pull\n>      +    request #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\n>           will be wired up in subsequent commits.\n>       \n>           Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>      @@ t/unit-tests/clar/clar/fs.h (new)\n>       +\t\t\tERROR_PATH_NOT_FOUND == last_error)\n>       +\t\t\treturn 0;\n>       +\n>      -+\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n>      ++\t\tSleep(RM_RETRY_DELAY * retries * retries);\n>       +\t}\n>       +\twhile (retries++ <= RM_RETRY_COUNT);\n>       +\n>      @@ t/unit-tests/clar/clar/sandbox.h (new)\n>       +\tstatic const size_t var_count = 5;\n>       +\tstatic const char *env_vars[] = {\n>       +\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n>      -+ \t};\n>      ++\t};\n>       +\n>      -+ \tsize_t i;\n>      ++\tsize_t i;\n>       +\n>       +\tfor (i = 0; i < var_count; ++i) {\n>       +\t\tconst char *env = getenv(env_vars[i]);\n>      @@ t/unit-tests/clar/clar/sandbox.h (new)\n>       +{\n>       +\treturn _clar_path;\n>       +}\n>      -+\n>       \n>        ## t/unit-tests/clar/clar/summary.h (new) ##\n>       @@\n>      @@ t/unit-tests/clar/generate.py (new)\n>       +    suite.disable(options.excluded)\n>       +    if suite.write():\n>       +        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n>      -+\n>       \n>        ## t/unit-tests/clar/test/.gitignore (new) ##\n>       @@\n>      @@ t/unit-tests/clar/test/.gitignore (new)\n>       +.clarcache\n>       +clar_test\n>       +*.o\n>      -+\n>       \n>        ## t/unit-tests/clar/test/Makefile (new) ##\n>       @@\n>   4:  75e097dfa4 =  3:  db53673294 t/clar: fix compatibility with NonStop\n>   3:  e0f99874cc !  4:  b6199c88dd t/clar: fix whitespace errors\n>      @@ Metadata\n>       Author: Patrick Steinhardt <ps@pks.im>\n>       \n>        ## Commit message ##\n>      -    t/clar: fix whitespace errors\n>      -\n>      -    Fix whitespace errors in the clar that make git-apply(1) unhappy. This\n>      -    has been cherry-picked from the upstream pull request at [1].\n>      -\n>      -    [1]: https://github.com/clar-test/clar/pull/97\n>      +    Makefile: fix sparse dependency on GENERATED_H\n>      +\n>      +    The \"check\" Makefile target is essentially an alias around the \"sparse\"\n>      +    target. The one difference though is that it will tell users to instead\n>      +    run the \"test\" target in case they do not have sparse(1) installed, as\n>      +    chances are high that they wanted to execute the test suite rather than\n>      +    doing semantic checks.\n>      +\n>      +    But even though the \"check\" target ultimately just ends up executing\n>      +    `make sparse`, it still depends on our generated headers. This does not\n>      +    make any sense though: they are irrelevant for the \"test\" target advice,\n>      +    and if these headers are required for the \"sparse\" target they must be\n>      +    declared as a dependency on the aliased target, not the alias.\n>      +\n>      +    But even moving the dependency to the \"sparse\" target is wrong, as\n>      +    concurrent builds may then end up generating the headers and running\n>      +    sparse concurrently. Instead, we make them a dependency of the specific\n>      +    objects. While that is overly broad, it does ensure correct ordering.\n>      +    The alternative, specifying which file depends on what generated header\n>      +    explicitly, feels rather unmaintainable.\n>       \n>           Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>       \n>      - ## t/unit-tests/clar/clar/fs.h ##\n>      -@@ t/unit-tests/clar/clar/fs.h: fs_rm_wait(WCHAR *_wpath)\n>      - \t\t\tERROR_PATH_NOT_FOUND == last_error)\n>      - \t\t\treturn 0;\n>      -\n>      --\t\tSleep(RM_RETRY_DELAY * retries * retries);\t\n>      -+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n>      - \t}\n>      - \twhile (retries++ <= RM_RETRY_COUNT);\n>      + ## Makefile ##\n>      +@@ Makefile: check-sha1:: t/helper/test-tool$X\n>        \n>      -\n>      - ## t/unit-tests/clar/clar/sandbox.h ##\n>      -@@ t/unit-tests/clar/clar/sandbox.h: find_tmp_path(char *buffer, size_t length)\n>      - \tstatic const size_t var_count = 5;\n>      - \tstatic const char *env_vars[] = {\n>      - \t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n>      -- \t};\n>      -+\t};\n>      + SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n>        \n>      -- \tsize_t i;\n>      -+\tsize_t i;\n>      +-$(SP_OBJ): %.sp: %.c %.o\n>      ++$(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n>      + \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n>      + \t\t-Wsparse-error \\\n>      + \t\t$(SPARSE_FLAGS) $(SP_EXTRA_FLAGS) $< && \\\n>      +@@ Makefile: style:\n>      + \tgit clang-format --style file --diff --extensions c,h\n>        \n>      - \tfor (i = 0; i < var_count; ++i) {\n>      - \t\tconst char *env = getenv(env_vars[i]);\n>      -@@ t/unit-tests/clar/clar/sandbox.h: const char *clar_sandbox_path(void)\n>      - {\n>      - \treturn _clar_path;\n>      - }\n>      --\n>      -\n>      - ## t/unit-tests/clar/generate.py ##\n>      -@@ t/unit-tests/clar/generate.py: def write(self):\n>      -     suite.disable(options.excluded)\n>      -     if suite.write():\n>      -         print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n>      --\n>      -\n>      - ## t/unit-tests/clar/test/.gitignore ##\n>      -@@ t/unit-tests/clar/test/.gitignore: clar.suite\n>      - .clarcache\n>      - clar_test\n>      - *.o\n>      --\n>      + .PHONY: check\n>      +-check: $(GENERATED_H)\n>      ++check:\n>      + \t@if sparse; \\\n>      + \tthen \\\n>      + \t\techo >&2 \"Use 'make sparse' instead\"; \\\n>   -:  ---------- >  5:  06364b2b72 Makefile: make hdr-check depend on generated headers\n>   -:  ---------- >  6:  88ea94ce16 Makefile: do not use sparse on third-party sources\n>   5:  5b8a64ae79 !  7:  05bcb5bef6 Makefile: wire up the clar unit testing framework\n>      @@ .gitignore\n>        /bin-wrappers/\n>       \n>        ## Makefile ##\n>      +@@ Makefile: REFTABLE_TEST_LIB = reftable/libreftable_test.a\n>      + GENERATED_H += command-list.h\n>      + GENERATED_H += config-list.h\n>      + GENERATED_H += hook-list.h\n>      ++GENERATED_H += $(UNIT_TEST_DIR)/clar-decls.h\n>      ++GENERATED_H += $(UNIT_TEST_DIR)/clar.suite\n>      +\n>      + .PHONY: generated-hdrs\n>      + generated-hdrs: $(GENERATED_H)\n>       @@ Makefile: THIRD_PARTY_SOURCES += sha1dc/%\n>        THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n>        THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n>      @@ Makefile: endif\n>        \n>        bin-wrappers/%: wrap-for-bin.sh\n>        \t$(call mkdir_p_parent_template)\n>      -@@ Makefile: CHK_HDRS = $(filter-out $(EXCEPT_HDRS),$(LIB_H))\n>      - HCO = $(patsubst %.h,%.hco,$(CHK_HDRS))\n>      - HCC = $(HCO:hco=hcc)\n>      -\n>      -+$(UNIT_TEST_DIR)/unit-test.hcc: $(UNIT_TEST_DIR)/unit-test.h $(UNIT_TEST_DIR)/clar-decls.h\n>      - %.hcc: %.h\n>      - \t@echo '#include \"git-compat-util.h\"' >$@\n>      - \t@echo '#include \"$<\"' >>$@\n>       @@ Makefile: endif\n>        \n>        artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n>      @@ Makefile: cocciclean:\n>        \n>        clean: profile-clean coverage-clean cocciclean\n>        \t$(RM) -r .build $(UNIT_TEST_BIN)\n>      -+\t$(RM) GIT-TEST-SUITES $(UNIT_TEST_DIR)/clar.suite $(UNIT_TEST_DIR)/clar-decls.h\n>      ++\t$(RM) GIT-TEST-SUITES\n>        \t$(RM) po/git.pot po/git-core.pot\n>        \t$(RM) git.res\n>        \t$(RM) $(OBJECTS)\n>   6:  bc4e23d666 =  8:  8f56b4d626 t/unit-tests: convert strvec tests to use clar\n>   7:  0a7fe8775a =  9:  ca09d19fd5 t/unit-tests: convert ctype tests to use clar\n> \n> base-commit: 406f326d271e0bacecdb00425422c5fa3f314930\n"},{"id":"501138","messageId":"831543c6-2282-44f7-bf05-b66c2d6b08cc@gmail.com","threadId":"61874","inReplyTo":"369060793306daab1796ae3063b4eaacc287f12d.1723791831.git.ps@pks.im","subject":"Re: [PATCH v5 2/9] t: import the clar unit testing framework","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-16T13:37:52Z","receivedAt":"2024-08-16T13:37:56Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Johannes - are these format specifiers supported by the version of \nMSVCRT that is used by git for windows? c.f. \nhttps://lore.kernel.org/git/nycvar.QRO.7.76.6.2203241456250.388@tvgsbejvaqbjf.bet/\n\nOn 16/08/2024 08:04, Patrick Steinhardt wrote:\n> diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\n> [...]\n> +#ifdef _WIN32\n > [...]\n> +#\tifndef PRIuZ\n> +#\t\tdefine PRIuZ \"Iu\"\n> +#\tendif\n> +#\tifndef PRIxZ\n> +#\t\tdefine PRIxZ \"Ix\"\n> +#\tendif\n\nThanks\n\nPhillip\n\n> +#\tif defined(_MSC_VER) || defined(__MINGW32__)\n> +\ttypedef struct stat STAT_T;\n> +#\telse\n> +\ttypedef struct _stat STAT_T;\n> +#\tendif\n> +#else\n> +#\tinclude <sys/wait.h> /* waitpid(2) */\n> +#\tinclude <unistd.h>\n> +#\tdefine _MAIN_CC\n> +#\tdefine p_snprintf snprintf\n> +#\tifndef PRIuZ\n> +#\t\tdefine PRIuZ \"zu\"\n> +#\tendif\n> +#\tifndef PRIxZ\n> +#\t\tdefine PRIxZ \"zx\"\n> +#\tendif\n> +\ttypedef struct stat STAT_T;\n> +#endif\n> +\n> +#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n> +\n> +#include \"clar.h\"\n> +\n> +static void fs_rm(const char *_source);\n> +static void fs_copy(const char *_source, const char *dest);\n> +\n> +#ifdef CLAR_FIXTURE_PATH\n> +static const char *\n> +fixture_path(const char *base, const char *fixture_name);\n> +#endif\n> +\n> +struct clar_error {\n> +\tconst char *file;\n> +\tconst char *function;\n> +\tsize_t line_number;\n> +\tconst char *error_msg;\n> +\tchar *description;\n> +\n> +\tstruct clar_error *next;\n> +};\n> +\n> +struct clar_explicit {\n> +\tsize_t suite_idx;\n> +\tconst char *filter;\n> +\n> +\tstruct clar_explicit *next;\n> +};\n> +\n> +struct clar_report {\n> +\tconst char *test;\n> +\tint test_number;\n> +\tconst char *suite;\n> +\n> +\tenum cl_test_status status;\n> +\ttime_t start;\n> +\tdouble elapsed;\n> +\n> +\tstruct clar_error *errors;\n> +\tstruct clar_error *last_error;\n> +\n> +\tstruct clar_report *next;\n> +};\n> +\n> +struct clar_summary {\n> +\tconst char *filename;\n> +\tFILE *fp;\n> +};\n> +\n> +static struct {\n> +\tenum cl_test_status test_status;\n> +\n> +\tconst char *active_test;\n> +\tconst char *active_suite;\n> +\n> +\tint total_skipped;\n> +\tint total_errors;\n> +\n> +\tint tests_ran;\n> +\tint suites_ran;\n> +\n> +\tenum cl_output_format output_format;\n> +\n> +\tint report_errors_only;\n> +\tint exit_on_error;\n> +\tint verbosity;\n> +\n> +\tint write_summary;\n> +\tchar *summary_filename;\n> +\tstruct clar_summary *summary;\n> +\n> +\tstruct clar_explicit *explicit;\n> +\tstruct clar_explicit *last_explicit;\n> +\n> +\tstruct clar_report *reports;\n> +\tstruct clar_report *last_report;\n> +\n> +\tvoid (*local_cleanup)(void *);\n> +\tvoid *local_cleanup_payload;\n> +\n> +\tjmp_buf trampoline;\n> +\tint trampoline_enabled;\n> +\n> +\tcl_trace_cb *pfn_trace_cb;\n> +\tvoid *trace_payload;\n> +\n> +} _clar;\n> +\n> +struct clar_func {\n> +\tconst char *name;\n> +\tvoid (*ptr)(void);\n> +};\n> +\n> +struct clar_suite {\n> +\tconst char *name;\n> +\tstruct clar_func initialize;\n> +\tstruct clar_func cleanup;\n> +\tconst struct clar_func *tests;\n> +\tsize_t test_count;\n> +\tint enabled;\n> +};\n> +\n> +/* From clar_print_*.c */\n> +static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n> +static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n> +static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n> +static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n> +static void clar_print_onsuite(const char *suite_name, int suite_index);\n> +static void clar_print_onabort(const char *msg, ...);\n> +\n> +/* From clar_sandbox.c */\n> +static void clar_unsandbox(void);\n> +static int clar_sandbox(void);\n> +\n> +/* From summary.h */\n> +static struct clar_summary *clar_summary_init(const char *filename);\n> +static int clar_summary_shutdown(struct clar_summary *fp);\n> +\n> +/* Load the declarations for the test suite */\n> +#include \"clar.suite\"\n> +\n> +\n> +#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n> +\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n> +\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n> +\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n> +\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n> +\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n> +\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n> +\t} while (0)\n> +\n> +void cl_trace_register(cl_trace_cb *cb, void *payload)\n> +{\n> +\t_clar.pfn_trace_cb = cb;\n> +\t_clar.trace_payload = payload;\n> +}\n> +\n> +\n> +/* Core test functions */\n> +static void\n> +clar_report_errors(struct clar_report *report)\n> +{\n> +\tstruct clar_error *error;\n> +\tint i = 1;\n> +\n> +\tfor (error = report->errors; error; error = error->next)\n> +\t\tclar_print_error(i++, _clar.last_report, error);\n> +}\n> +\n> +static void\n> +clar_report_all(void)\n> +{\n> +\tstruct clar_report *report;\n> +\tstruct clar_error *error;\n> +\tint i = 1;\n> +\n> +\tfor (report = _clar.reports; report; report = report->next) {\n> +\t\tif (report->status != CL_TEST_FAILURE)\n> +\t\t\tcontinue;\n> +\n> +\t\tfor (error = report->errors; error; error = error->next)\n> +\t\t\tclar_print_error(i++, report, error);\n> +\t}\n> +}\n> +\n> +#ifdef WIN32\n> +# define clar_time DWORD\n> +\n> +static void clar_time_now(clar_time *out)\n> +{\n> +\t*out = GetTickCount();\n> +}\n> +\n> +static double clar_time_diff(clar_time *start, clar_time *end)\n> +{\n> +\treturn ((double)*end - (double)*start) / 1000;\n> +}\n> +#else\n> +# include <sys/time.h>\n> +\n> +# define clar_time struct timeval\n> +\n> +static void clar_time_now(clar_time *out)\n> +{\n> +\tstruct timezone tz;\n> +\n> +\tgettimeofday(out, &tz);\n> +}\n> +\n> +static double clar_time_diff(clar_time *start, clar_time *end)\n> +{\n> +\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n> +\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n> +}\n> +#endif\n> +\n> +static void\n> +clar_run_test(\n> +\tconst struct clar_suite *suite,\n> +\tconst struct clar_func *test,\n> +\tconst struct clar_func *initialize,\n> +\tconst struct clar_func *cleanup)\n> +{\n> +\tclar_time start, end;\n> +\n> +\t_clar.trampoline_enabled = 1;\n> +\n> +\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n> +\n> +\t_clar.last_report->start = time(NULL);\n> +\tclar_time_now(&start);\n> +\n> +\tif (setjmp(_clar.trampoline) == 0) {\n> +\t\tif (initialize->ptr != NULL)\n> +\t\t\tinitialize->ptr();\n> +\n> +\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n> +\t\ttest->ptr();\n> +\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n> +\t}\n> +\n> +\tclar_time_now(&end);\n> +\n> +\t_clar.trampoline_enabled = 0;\n> +\n> +\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n> +\t\t_clar.last_report->status = CL_TEST_OK;\n> +\n> +\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n> +\n> +\tif (_clar.local_cleanup != NULL)\n> +\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n> +\n> +\tif (cleanup->ptr != NULL)\n> +\t\tcleanup->ptr();\n> +\n> +\tCL_TRACE(CL_TRACE__TEST__END);\n> +\n> +\t_clar.tests_ran++;\n> +\n> +\t/* remove any local-set cleanup methods */\n> +\t_clar.local_cleanup = NULL;\n> +\t_clar.local_cleanup_payload = NULL;\n> +\n> +\tif (_clar.report_errors_only) {\n> +\t\tclar_report_errors(_clar.last_report);\n> +\t} else {\n> +\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n> +\t}\n> +}\n> +\n> +static void\n> +clar_run_suite(const struct clar_suite *suite, const char *filter)\n> +{\n> +\tconst struct clar_func *test = suite->tests;\n> +\tsize_t i, matchlen;\n> +\tstruct clar_report *report;\n> +\tint exact = 0;\n> +\n> +\tif (!suite->enabled)\n> +\t\treturn;\n> +\n> +\tif (_clar.exit_on_error && _clar.total_errors)\n> +\t\treturn;\n> +\n> +\tif (!_clar.report_errors_only)\n> +\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n> +\n> +\t_clar.active_suite = suite->name;\n> +\t_clar.active_test = NULL;\n> +\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n> +\n> +\tif (filter) {\n> +\t\tsize_t suitelen = strlen(suite->name);\n> +\t\tmatchlen = strlen(filter);\n> +\t\tif (matchlen <= suitelen) {\n> +\t\t\tfilter = NULL;\n> +\t\t} else {\n> +\t\t\tfilter += suitelen;\n> +\t\t\twhile (*filter == ':')\n> +\t\t\t\t++filter;\n> +\t\t\tmatchlen = strlen(filter);\n> +\n> +\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n> +\t\t\t\texact = 1;\n> +\t\t\t\tmatchlen--;\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\n> +\tfor (i = 0; i < suite->test_count; ++i) {\n> +\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n> +\t\t\tcontinue;\n> +\n> +\t\tif (exact && strlen(test[i].name) != matchlen)\n> +\t\t\tcontinue;\n> +\n> +\t\t_clar.active_test = test[i].name;\n> +\n> +\t\treport = calloc(1, sizeof(struct clar_report));\n> +\t\treport->suite = _clar.active_suite;\n> +\t\treport->test = _clar.active_test;\n> +\t\treport->test_number = _clar.tests_ran;\n> +\t\treport->status = CL_TEST_NOTRUN;\n> +\n> +\t\tif (_clar.reports == NULL)\n> +\t\t\t_clar.reports = report;\n> +\n> +\t\tif (_clar.last_report != NULL)\n> +\t\t\t_clar.last_report->next = report;\n> +\n> +\t\t_clar.last_report = report;\n> +\n> +\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n> +\n> +\t\tif (_clar.exit_on_error && _clar.total_errors)\n> +\t\t\treturn;\n> +\t}\n> +\n> +\t_clar.active_test = NULL;\n> +\tCL_TRACE(CL_TRACE__SUITE_END);\n> +}\n> +\n> +static void\n> +clar_usage(const char *arg)\n> +{\n> +\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n> +\tprintf(\"Options:\\n\");\n> +\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n> +\tprintf(\"  -iname        Include the suite with `name`\\n\");\n> +\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n> +\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n> +\tprintf(\"  -q            Only report tests that had an error\\n\");\n> +\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n> +\tprintf(\"  -t            Display results in tap format\\n\");\n> +\tprintf(\"  -l            Print suite names\\n\");\n> +\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n> +\texit(-1);\n> +}\n> +\n> +static void\n> +clar_parse_args(int argc, char **argv)\n> +{\n> +\tint i;\n> +\n> +\t/* Verify options before execute */\n> +\tfor (i = 1; i < argc; ++i) {\n> +\t\tchar *argument = argv[i];\n> +\n> +\t\tif (argument[0] != '-' || argument[1] == '\\0'\n> +\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n> +\t\t\tclar_usage(argv[0]);\n> +\t\t}\n> +\t}\n> +\n> +\tfor (i = 1; i < argc; ++i) {\n> +\t\tchar *argument = argv[i];\n> +\n> +\t\tswitch (argument[1]) {\n> +\t\tcase 's':\n> +\t\tcase 'i':\n> +\t\tcase 'x': { /* given suite name */\n> +\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n> +\t\t\tchar action = argument[1];\n> +\t\t\tsize_t j, arglen, suitelen, cmplen;\n> +\n> +\t\t\targument += offset;\n> +\t\t\targlen = strlen(argument);\n> +\n> +\t\t\tif (arglen == 0)\n> +\t\t\t\tclar_usage(argv[0]);\n> +\n> +\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n> +\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n> +\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n> +\n> +\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n> +\t\t\t\t\tint exact = (arglen >= suitelen);\n> +\n> +\t\t\t\t\t/* Do we have a real suite prefix separated by a\n> +\t\t\t\t\t * trailing '::' or just a matching substring? */\n> +\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n> +\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n> +\t\t\t\t\t    continue;\n> +\n> +\t\t\t\t\t++found;\n> +\n> +\t\t\t\t\tif (!exact)\n> +\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n> +\n> +\t\t\t\t\tswitch (action) {\n> +\t\t\t\t\tcase 's': {\n> +\t\t\t\t\t\tstruct clar_explicit *explicit =\n> +\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n> +\t\t\t\t\t\tassert(explicit);\n> +\n> +\t\t\t\t\t\texplicit->suite_idx = j;\n> +\t\t\t\t\t\texplicit->filter = argument;\n> +\n> +\t\t\t\t\t\tif (_clar.explicit == NULL)\n> +\t\t\t\t\t\t\t_clar.explicit = explicit;\n> +\n> +\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n> +\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n> +\n> +\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n> +\t\t\t\t\t\t_clar.last_explicit = explicit;\n> +\t\t\t\t\t\tbreak;\n> +\t\t\t\t\t}\n> +\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n> +\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n> +\t\t\t\t\t}\n> +\n> +\t\t\t\t\tif (exact)\n> +\t\t\t\t\t\tbreak;\n> +\t\t\t\t}\n> +\t\t\t}\n> +\n> +\t\t\tif (!found) {\n> +\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n> +\t\t\t\texit(-1);\n> +\t\t\t}\n> +\t\t\tbreak;\n> +\t\t}\n> +\n> +\t\tcase 'q':\n> +\t\t\t_clar.report_errors_only = 1;\n> +\t\t\tbreak;\n> +\n> +\t\tcase 'Q':\n> +\t\t\t_clar.exit_on_error = 1;\n> +\t\t\tbreak;\n> +\n> +\t\tcase 't':\n> +\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n> +\t\t\tbreak;\n> +\n> +\t\tcase 'l': {\n> +\t\t\tsize_t j;\n> +\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n> +\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n> +\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n> +\n> +\t\t\texit(0);\n> +\t\t}\n> +\n> +\t\tcase 'v':\n> +\t\t\t_clar.verbosity++;\n> +\t\t\tbreak;\n> +\n> +\t\tcase 'r':\n> +\t\t\t_clar.write_summary = 1;\n> +\t\t\tfree(_clar.summary_filename);\n> +\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n> +\t\t\tbreak;\n> +\n> +\t\tdefault:\n> +\t\t\tassert(!\"Unexpected commandline argument!\");\n> +\t\t}\n> +\t}\n> +}\n> +\n> +void\n> +clar_test_init(int argc, char **argv)\n> +{\n> +\tconst char *summary_env;\n> +\n> +\tif (argc > 1)\n> +\t\tclar_parse_args(argc, argv);\n> +\n> +\tclar_print_init(\n> +\t\t(int)_clar_callback_count,\n> +\t\t(int)_clar_suite_count,\n> +\t\t\"\"\n> +\t);\n> +\n> +\tif (!_clar.summary_filename &&\n> +\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n> +\t\t_clar.write_summary = 1;\n> +\t\t_clar.summary_filename = strdup(summary_env);\n> +\t}\n> +\n> +\tif (_clar.write_summary && !_clar.summary_filename)\n> +\t\t_clar.summary_filename = strdup(\"summary.xml\");\n> +\n> +\tif (_clar.write_summary &&\n> +\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n> +\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n> +\t\texit(-1);\n> +\t}\n> +\n> +\tif (clar_sandbox() < 0) {\n> +\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n> +\t\texit(-1);\n> +\t}\n> +}\n> +\n> +int\n> +clar_test_run(void)\n> +{\n> +\tsize_t i;\n> +\tstruct clar_explicit *explicit;\n> +\n> +\tif (_clar.explicit) {\n> +\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n> +\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n> +\t} else {\n> +\t\tfor (i = 0; i < _clar_suite_count; ++i)\n> +\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n> +\t}\n> +\n> +\treturn _clar.total_errors;\n> +}\n> +\n> +void\n> +clar_test_shutdown(void)\n> +{\n> +\tstruct clar_explicit *explicit, *explicit_next;\n> +\tstruct clar_report *report, *report_next;\n> +\n> +\tclar_print_shutdown(\n> +\t\t_clar.tests_ran,\n> +\t\t(int)_clar_suite_count,\n> +\t\t_clar.total_errors\n> +\t);\n> +\n> +\tclar_unsandbox();\n> +\n> +\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n> +\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n> +\t\texit(-1);\n> +\t}\n> +\n> +\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n> +\t\texplicit_next = explicit->next;\n> +\t\tfree(explicit);\n> +\t}\n> +\n> +\tfor (report = _clar.reports; report; report = report_next) {\n> +\t\treport_next = report->next;\n> +\t\tfree(report);\n> +\t}\n> +\n> +\tfree(_clar.summary_filename);\n> +}\n> +\n> +int\n> +clar_test(int argc, char **argv)\n> +{\n> +\tint errors;\n> +\n> +\tclar_test_init(argc, argv);\n> +\terrors = clar_test_run();\n> +\tclar_test_shutdown();\n> +\n> +\treturn errors;\n> +}\n> +\n> +static void abort_test(void)\n> +{\n> +\tif (!_clar.trampoline_enabled) {\n> +\t\tclar_print_onabort(\n> +\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n> +\t\tclar_report_errors(_clar.last_report);\n> +\t\texit(-1);\n> +\t}\n> +\n> +\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n> +\tlongjmp(_clar.trampoline, -1);\n> +}\n> +\n> +void clar__skip(void)\n> +{\n> +\t_clar.last_report->status = CL_TEST_SKIP;\n> +\t_clar.total_skipped++;\n> +\tabort_test();\n> +}\n> +\n> +void clar__fail(\n> +\tconst char *file,\n> +\tconst char *function,\n> +\tsize_t line,\n> +\tconst char *error_msg,\n> +\tconst char *description,\n> +\tint should_abort)\n> +{\n> +\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n> +\n> +\tif (_clar.last_report->errors == NULL)\n> +\t\t_clar.last_report->errors = error;\n> +\n> +\tif (_clar.last_report->last_error != NULL)\n> +\t\t_clar.last_report->last_error->next = error;\n> +\n> +\t_clar.last_report->last_error = error;\n> +\n> +\terror->file = file;\n> +\terror->function = function;\n> +\terror->line_number = line;\n> +\terror->error_msg = error_msg;\n> +\n> +\tif (description != NULL)\n> +\t\terror->description = strdup(description);\n> +\n> +\t_clar.total_errors++;\n> +\t_clar.last_report->status = CL_TEST_FAILURE;\n> +\n> +\tif (should_abort)\n> +\t\tabort_test();\n> +}\n> +\n> +void clar__assert(\n> +\tint condition,\n> +\tconst char *file,\n> +\tconst char *function,\n> +\tsize_t line,\n> +\tconst char *error_msg,\n> +\tconst char *description,\n> +\tint should_abort)\n> +{\n> +\tif (condition)\n> +\t\treturn;\n> +\n> +\tclar__fail(file, function, line, error_msg, description, should_abort);\n> +}\n> +\n> +void clar__assert_equal(\n> +\tconst char *file,\n> +\tconst char *function,\n> +\tsize_t line,\n> +\tconst char *err,\n> +\tint should_abort,\n> +\tconst char *fmt,\n> +\t...)\n> +{\n> +\tva_list args;\n> +\tchar buf[4096];\n> +\tint is_equal = 1;\n> +\n> +\tva_start(args, fmt);\n> +\n> +\tif (!strcmp(\"%s\", fmt)) {\n> +\t\tconst char *s1 = va_arg(args, const char *);\n> +\t\tconst char *s2 = va_arg(args, const char *);\n> +\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n> +\n> +\t\tif (!is_equal) {\n> +\t\t\tif (s1 && s2) {\n> +\t\t\t\tint pos;\n> +\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n> +\t\t\t\t\t/* find differing byte offset */;\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n> +\t\t\t\t\ts1, s2, pos);\n> +\t\t\t} else {\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\telse if(!strcmp(\"%.*s\", fmt)) {\n> +\t\tconst char *s1 = va_arg(args, const char *);\n> +\t\tconst char *s2 = va_arg(args, const char *);\n> +\t\tint len = va_arg(args, int);\n> +\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n> +\n> +\t\tif (!is_equal) {\n> +\t\t\tif (s1 && s2) {\n> +\t\t\t\tint pos;\n> +\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n> +\t\t\t\t\t/* find differing byte offset */;\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n> +\t\t\t\t\tlen, s1, len, s2, pos);\n> +\t\t\t} else {\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\telse if (!strcmp(\"%ls\", fmt)) {\n> +\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n> +\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n> +\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n> +\n> +\t\tif (!is_equal) {\n> +\t\t\tif (wcs1 && wcs2) {\n> +\t\t\t\tint pos;\n> +\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n> +\t\t\t\t\t/* find differing byte offset */;\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n> +\t\t\t\t\twcs1, wcs2, pos);\n> +\t\t\t} else {\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\telse if(!strcmp(\"%.*ls\", fmt)) {\n> +\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n> +\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n> +\t\tint len = va_arg(args, int);\n> +\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n> +\n> +\t\tif (!is_equal) {\n> +\t\t\tif (wcs1 && wcs2) {\n> +\t\t\t\tint pos;\n> +\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n> +\t\t\t\t\t/* find differing byte offset */;\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n> +\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n> +\t\t\t} else {\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n> +\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n> +\t\tis_equal = (sz1 == sz2);\n> +\t\tif (!is_equal) {\n> +\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n> +\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n> +\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n> +\t\t}\n> +\t}\n> +\telse if (!strcmp(\"%p\", fmt)) {\n> +\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n> +\t\tis_equal = (p1 == p2);\n> +\t\tif (!is_equal)\n> +\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n> +\t}\n> +\telse {\n> +\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n> +\t\tis_equal = (i1 == i2);\n> +\t\tif (!is_equal) {\n> +\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n> +\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n> +\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n> +\t\t}\n> +\t}\n> +\n> +\tva_end(args);\n> +\n> +\tif (!is_equal)\n> +\t\tclar__fail(file, function, line, err, buf, should_abort);\n> +}\n> +\n> +void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n> +{\n> +\t_clar.local_cleanup = cleanup;\n> +\t_clar.local_cleanup_payload = opaque;\n> +}\n> +\n> +#include \"clar/sandbox.h\"\n> +#include \"clar/fixtures.h\"\n> +#include \"clar/fs.h\"\n> +#include \"clar/print.h\"\n> +#include \"clar/summary.h\"\n> diff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\n> new file mode 100644\n> index 0000000000..8c22382bd5\n> --- /dev/null\n> +++ b/t/unit-tests/clar/clar.h\n> @@ -0,0 +1,173 @@\n> +/*\n> + * Copyright (c) Vicent Marti. All rights reserved.\n> + *\n> + * This file is part of clar, distributed under the ISC license.\n> + * For full terms see the included COPYING file.\n> + */\n> +#ifndef __CLAR_TEST_H__\n> +#define __CLAR_TEST_H__\n> +\n> +#include <stdlib.h>\n> +\n> +enum cl_test_status {\n> +\tCL_TEST_OK,\n> +\tCL_TEST_FAILURE,\n> +\tCL_TEST_SKIP,\n> +\tCL_TEST_NOTRUN,\n> +};\n> +\n> +enum cl_output_format {\n> +\tCL_OUTPUT_CLAP,\n> +\tCL_OUTPUT_TAP,\n> +};\n> +\n> +/** Setup clar environment */\n> +void clar_test_init(int argc, char *argv[]);\n> +int clar_test_run(void);\n> +void clar_test_shutdown(void);\n> +\n> +/** One shot setup & run */\n> +int clar_test(int argc, char *argv[]);\n> +\n> +const char *clar_sandbox_path(void);\n> +\n> +void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n> +void cl_fs_cleanup(void);\n> +\n> +/**\n> + * cl_trace_* is a hook to provide a simple global tracing\n> + * mechanism.\n> + *\n> + * The goal here is to let main() provide clar-proper\n> + * with a callback to optionally write log info for\n> + * test operations into the same stream used by their\n> + * actual tests.  This would let them print test names\n> + * and maybe performance data as they choose.\n> + *\n> + * The goal is NOT to alter the flow of control or to\n> + * override test selection/skipping.  (So the callback\n> + * does not return a value.)\n> + *\n> + * The goal is NOT to duplicate the existing\n> + * pass/fail/skip reporting.  (So the callback\n> + * does not accept a status/errorcode argument.)\n> + *\n> + */\n> +typedef enum cl_trace_event {\n> +\tCL_TRACE__SUITE_BEGIN,\n> +\tCL_TRACE__SUITE_END,\n> +\tCL_TRACE__TEST__BEGIN,\n> +\tCL_TRACE__TEST__END,\n> +\tCL_TRACE__TEST__RUN_BEGIN,\n> +\tCL_TRACE__TEST__RUN_END,\n> +\tCL_TRACE__TEST__LONGJMP,\n> +} cl_trace_event;\n> +\n> +typedef void (cl_trace_cb)(\n> +\tcl_trace_event ev,\n> +\tconst char *suite_name,\n> +\tconst char *test_name,\n> +\tvoid *payload);\n> +\n> +/**\n> + * Register a callback into CLAR to send global trace events.\n> + * Pass NULL to disable.\n> + */\n> +void cl_trace_register(cl_trace_cb *cb, void *payload);\n> +\n> +\n> +#ifdef CLAR_FIXTURE_PATH\n> +const char *cl_fixture(const char *fixture_name);\n> +void cl_fixture_sandbox(const char *fixture_name);\n> +void cl_fixture_cleanup(const char *fixture_name);\n> +const char *cl_fixture_basename(const char *fixture_name);\n> +#endif\n> +\n> +/**\n> + * Assertion macros with explicit error message\n> + */\n> +#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n> +#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n> +#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n> +\n> +/**\n> + * Check macros with explicit error message\n> + */\n> +#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n> +#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n> +#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n> +\n> +/**\n> + * Assertion macros with no error message\n> + */\n> +#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n> +#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n> +#define cl_assert(expr) cl_assert_(expr, NULL)\n> +\n> +/**\n> + * Check macros with no error message\n> + */\n> +#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n> +#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n> +#define cl_check(expr) cl_check_(expr, NULL)\n> +\n> +/**\n> + * Forced failure/warning\n> + */\n> +#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n> +#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n> +\n> +#define cl_skip() clar__skip()\n> +\n> +/**\n> + * Typed assertion macros\n> + */\n> +#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n> +#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n> +\n> +#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n> +#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n> +\n> +#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n> +#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n> +\n> +#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n> +#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n> +\n> +#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n> +#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n> +#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n> +\n> +#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n> +\n> +#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n> +\n> +void clar__skip(void);\n> +\n> +void clar__fail(\n> +\tconst char *file,\n> +\tconst char *func,\n> +\tsize_t line,\n> +\tconst char *error,\n> +\tconst char *description,\n> +\tint should_abort);\n> +\n> +void clar__assert(\n> +\tint condition,\n> +\tconst char *file,\n> +\tconst char *func,\n> +\tsize_t line,\n> +\tconst char *error,\n> +\tconst char *description,\n> +\tint should_abort);\n> +\n> +void clar__assert_equal(\n> +\tconst char *file,\n> +\tconst char *func,\n> +\tsize_t line,\n> +\tconst char *err,\n> +\tint should_abort,\n> +\tconst char *fmt,\n> +\t...);\n> +\n> +#endif\n> diff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\n> new file mode 100644\n> index 0000000000..6ec6423484\n> --- /dev/null\n> +++ b/t/unit-tests/clar/clar/fixtures.h\n> @@ -0,0 +1,50 @@\n> +#ifdef CLAR_FIXTURE_PATH\n> +static const char *\n> +fixture_path(const char *base, const char *fixture_name)\n> +{\n> +\tstatic char _path[4096];\n> +\tsize_t root_len;\n> +\n> +\troot_len = strlen(base);\n> +\tstrncpy(_path, base, sizeof(_path));\n> +\n> +\tif (_path[root_len - 1] != '/')\n> +\t\t_path[root_len++] = '/';\n> +\n> +\tif (fixture_name[0] == '/')\n> +\t\tfixture_name++;\n> +\n> +\tstrncpy(_path + root_len,\n> +\t\tfixture_name,\n> +\t\tsizeof(_path) - root_len);\n> +\n> +\treturn _path;\n> +}\n> +\n> +const char *cl_fixture(const char *fixture_name)\n> +{\n> +\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n> +}\n> +\n> +void cl_fixture_sandbox(const char *fixture_name)\n> +{\n> +\tfs_copy(cl_fixture(fixture_name), _clar_path);\n> +}\n> +\n> +const char *cl_fixture_basename(const char *fixture_name)\n> +{\n> +\tconst char *p;\n> +\n> +\tfor (p = fixture_name; *p; p++) {\n> +\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n> +\t\t\tfixture_name = p+1;\n> +\t}\n> +\n> +\treturn fixture_name;\n> +}\n> +\n> +void cl_fixture_cleanup(const char *fixture_name)\n> +{\n> +\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n> +}\n> +#endif\n> diff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\n> new file mode 100644\n> index 0000000000..3e39890bd3\n> --- /dev/null\n> +++ b/t/unit-tests/clar/clar/fs.h\n> @@ -0,0 +1,522 @@\n> +/*\n> + * By default, use a read/write loop to copy files on POSIX systems.\n> + * On Linux, use sendfile by default as it's slightly faster.  On\n> + * macOS, we avoid fcopyfile by default because it's slightly slower.\n> + */\n> +#undef USE_FCOPYFILE\n> +#define USE_SENDFILE 1\n> +\n> +#ifdef _WIN32\n> +\n> +#ifdef CLAR_WIN32_LONGPATHS\n> +# define CLAR_MAX_PATH 4096\n> +#else\n> +# define CLAR_MAX_PATH MAX_PATH\n> +#endif\n> +\n> +#define RM_RETRY_COUNT\t5\n> +#define RM_RETRY_DELAY\t10\n> +\n> +#ifdef __MINGW32__\n> +\n> +/* These security-enhanced functions are not available\n> + * in MinGW, so just use the vanilla ones */\n> +#define wcscpy_s(a, b, c) wcscpy((a), (c))\n> +#define wcscat_s(a, b, c) wcscat((a), (c))\n> +\n> +#endif /* __MINGW32__ */\n> +\n> +static int\n> +fs__dotordotdot(WCHAR *_tocheck)\n> +{\n> +\treturn _tocheck[0] == '.' &&\n> +\t\t(_tocheck[1] == '\\0' ||\n> +\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n> +}\n> +\n> +static int\n> +fs_rmdir_rmdir(WCHAR *_wpath)\n> +{\n> +\tunsigned retries = 1;\n> +\n> +\twhile (!RemoveDirectoryW(_wpath)) {\n> +\t\t/* Only retry when we have retries remaining, and the\n> +\t\t * error was ERROR_DIR_NOT_EMPTY. */\n> +\t\tif (retries++ > RM_RETRY_COUNT ||\n> +\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n> +\t\t\treturn -1;\n> +\n> +\t\t/* Give whatever has a handle to a child item some time\n> +\t\t * to release it before trying again */\n> +\t\tSleep(RM_RETRY_DELAY * retries * retries);\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +static void translate_path(WCHAR *path, size_t path_size)\n> +{\n> +    size_t path_len, i;\n> +\n> +    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n> +\treturn;\n> +\n> +    path_len = wcslen(path);\n> +    cl_assert(path_size > path_len + 4);\n> +\n> +    for (i = path_len; i > 0; i--) {\n> +\tWCHAR c = path[i - 1];\n> +\n> +\tif (c == L'/')\n> +\t    path[i + 3] = L'\\\\';\n> +\telse\n> +\t    path[i + 3] = path[i - 1];\n> +    }\n> +\n> +    path[0] = L'\\\\';\n> +    path[1] = L'\\\\';\n> +    path[2] = L'?';\n> +    path[3] = L'\\\\';\n> +    path[path_len + 4] = L'\\0';\n> +}\n> +\n> +static void\n> +fs_rmdir_helper(WCHAR *_wsource)\n> +{\n> +\tWCHAR buffer[CLAR_MAX_PATH];\n> +\tHANDLE find_handle;\n> +\tWIN32_FIND_DATAW find_data;\n> +\tsize_t buffer_prefix_len;\n> +\n> +\t/* Set up the buffer and capture the length */\n> +\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n> +\ttranslate_path(buffer, CLAR_MAX_PATH);\n> +\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n> +\tbuffer_prefix_len = wcslen(buffer);\n> +\n> +\t/* FindFirstFile needs a wildcard to match multiple items */\n> +\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n> +\tfind_handle = FindFirstFileW(buffer, &find_data);\n> +\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n> +\n> +\tdo {\n> +\t\t/* FindFirstFile/FindNextFile gives back . and ..\n> +\t\t * entries at the beginning */\n> +\t\tif (fs__dotordotdot(find_data.cFileName))\n> +\t\t\tcontinue;\n> +\n> +\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n> +\n> +\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n> +\t\t\tfs_rmdir_helper(buffer);\n> +\t\telse {\n> +\t\t\t/* If set, the +R bit must be cleared before deleting */\n> +\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n> +\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n> +\n> +\t\t\tcl_assert(DeleteFileW(buffer));\n> +\t\t}\n> +\t}\n> +\twhile (FindNextFileW(find_handle, &find_data));\n> +\n> +\t/* Ensure that we successfully completed the enumeration */\n> +\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n> +\n> +\t/* Close the find handle */\n> +\tFindClose(find_handle);\n> +\n> +\t/* Now that the directory is empty, remove it */\n> +\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n> +}\n> +\n> +static int\n> +fs_rm_wait(WCHAR *_wpath)\n> +{\n> +\tunsigned retries = 1;\n> +\tDWORD last_error;\n> +\n> +\tdo {\n> +\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n> +\t\t\tlast_error = GetLastError();\n> +\t\telse\n> +\t\t\tlast_error = ERROR_SUCCESS;\n> +\n> +\t\t/* Is the item gone? */\n> +\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n> +\t\t\tERROR_PATH_NOT_FOUND == last_error)\n> +\t\t\treturn 0;\n> +\n> +\t\tSleep(RM_RETRY_DELAY * retries * retries);\n> +\t}\n> +\twhile (retries++ <= RM_RETRY_COUNT);\n> +\n> +\treturn -1;\n> +}\n> +\n> +static void\n> +fs_rm(const char *_source)\n> +{\n> +\tWCHAR wsource[CLAR_MAX_PATH];\n> +\tDWORD attrs;\n> +\n> +\t/* The input path is UTF-8. Convert it to wide characters\n> +\t * for use with the Windows API */\n> +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n> +\t\t\t\tMB_ERR_INVALID_CHARS,\n> +\t\t\t\t_source,\n> +\t\t\t\t-1, /* Indicates NULL termination */\n> +\t\t\t\twsource,\n> +\t\t\t\tCLAR_MAX_PATH));\n> +\n> +\ttranslate_path(wsource, CLAR_MAX_PATH);\n> +\n> +\t/* Does the item exist? If not, we have no work to do */\n> +\tattrs = GetFileAttributesW(wsource);\n> +\n> +\tif (INVALID_FILE_ATTRIBUTES == attrs)\n> +\t\treturn;\n> +\n> +\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n> +\t\tfs_rmdir_helper(wsource);\n> +\telse {\n> +\t\t/* The item is a file. Strip the +R bit */\n> +\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n> +\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n> +\n> +\t\tcl_assert(DeleteFileW(wsource));\n> +\t}\n> +\n> +\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n> +\tcl_assert(0 == fs_rm_wait(wsource));\n> +}\n> +\n> +static void\n> +fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n> +{\n> +\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n> +\tHANDLE find_handle;\n> +\tWIN32_FIND_DATAW find_data;\n> +\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n> +\n> +\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n> +\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n> +\ttranslate_path(buf_source, CLAR_MAX_PATH);\n> +\tbuf_source_prefix_len = wcslen(buf_source);\n> +\n> +\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n> +\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n> +\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n> +\tbuf_dest_prefix_len = wcslen(buf_dest);\n> +\n> +\t/* Get an enumerator for the items in the source. */\n> +\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n> +\tfind_handle = FindFirstFileW(buf_source, &find_data);\n> +\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n> +\n> +\t/* Create the target directory. */\n> +\tcl_assert(CreateDirectoryW(_wdest, NULL));\n> +\n> +\tdo {\n> +\t\t/* FindFirstFile/FindNextFile gives back . and ..\n> +\t\t * entries at the beginning */\n> +\t\tif (fs__dotordotdot(find_data.cFileName))\n> +\t\t\tcontinue;\n> +\n> +\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n> +\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n> +\n> +\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n> +\t\t\tfs_copydir_helper(buf_source, buf_dest);\n> +\t\telse\n> +\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n> +\t}\n> +\twhile (FindNextFileW(find_handle, &find_data));\n> +\n> +\t/* Ensure that we successfully completed the enumeration */\n> +\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n> +\n> +\t/* Close the find handle */\n> +\tFindClose(find_handle);\n> +}\n> +\n> +static void\n> +fs_copy(const char *_source, const char *_dest)\n> +{\n> +\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n> +\tDWORD source_attrs, dest_attrs;\n> +\tHANDLE find_handle;\n> +\tWIN32_FIND_DATAW find_data;\n> +\n> +\t/* The input paths are UTF-8. Convert them to wide characters\n> +\t * for use with the Windows API. */\n> +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n> +\t\t\t\tMB_ERR_INVALID_CHARS,\n> +\t\t\t\t_source,\n> +\t\t\t\t-1,\n> +\t\t\t\twsource,\n> +\t\t\t\tCLAR_MAX_PATH));\n> +\n> +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n> +\t\t\t\tMB_ERR_INVALID_CHARS,\n> +\t\t\t\t_dest,\n> +\t\t\t\t-1,\n> +\t\t\t\twdest,\n> +\t\t\t\tCLAR_MAX_PATH));\n> +\n> +\ttranslate_path(wsource, CLAR_MAX_PATH);\n> +\ttranslate_path(wdest, CLAR_MAX_PATH);\n> +\n> +\t/* Check the source for existence */\n> +\tsource_attrs = GetFileAttributesW(wsource);\n> +\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n> +\n> +\t/* Check the target for existence */\n> +\tdest_attrs = GetFileAttributesW(wdest);\n> +\n> +\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n> +\t\t/* Target exists; append last path part of source to target.\n> +\t\t * Use FindFirstFile to parse the path */\n> +\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n> +\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n> +\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n> +\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n> +\t\tFindClose(find_handle);\n> +\n> +\t\t/* Check the new target for existence */\n> +\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n> +\t}\n> +\n> +\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n> +\t\tfs_copydir_helper(wsource, wdest);\n> +\telse\n> +\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n> +}\n> +\n> +void\n> +cl_fs_cleanup(void)\n> +{\n> +#ifdef CLAR_FIXTURE_PATH\n> +\tfs_rm(fixture_path(_clar_path, \"*\"));\n> +#endif\n> +}\n> +\n> +#else\n> +\n> +#include <errno.h>\n> +#include <string.h>\n> +#include <limits.h>\n> +#include <dirent.h>\n> +#include <fcntl.h>\n> +#include <unistd.h>\n> +#include <sys/types.h>\n> +#include <sys/stat.h>\n> +\n> +#if defined(__linux__)\n> +# include <sys/sendfile.h>\n> +#endif\n> +\n> +#if defined(__APPLE__)\n> +# include <copyfile.h>\n> +#endif\n> +\n> +static void basename_r(const char **out, int *out_len, const char *in)\n> +{\n> +\tsize_t in_len = strlen(in), start_pos;\n> +\n> +\tfor (in_len = strlen(in); in_len; in_len--) {\n> +\t\tif (in[in_len - 1] != '/')\n> +\t\t\tbreak;\n> +\t}\n> +\n> +\tfor (start_pos = in_len; start_pos; start_pos--) {\n> +\t\tif (in[start_pos - 1] == '/')\n> +\t\t\tbreak;\n> +\t}\n> +\n> +\tcl_assert(in_len - start_pos < INT_MAX);\n> +\n> +\tif (in_len - start_pos > 0) {\n> +\t\t*out = &in[start_pos];\n> +\t\t*out_len = (in_len - start_pos);\n> +\t} else {\n> +\t\t*out = \"/\";\n> +\t\t*out_len = 1;\n> +\t}\n> +}\n> +\n> +static char *joinpath(const char *dir, const char *base, int base_len)\n> +{\n> +\tchar *out;\n> +\tint len;\n> +\n> +\tif (base_len == -1) {\n> +\t\tsize_t bl = strlen(base);\n> +\n> +\t\tcl_assert(bl < INT_MAX);\n> +\t\tbase_len = (int)bl;\n> +\t}\n> +\n> +\tlen = strlen(dir) + base_len + 2;\n> +\tcl_assert(len > 0);\n> +\n> +\tcl_assert(out = malloc(len));\n> +\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n> +\n> +\treturn out;\n> +}\n> +\n> +static void\n> +fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n> +{\n> +\tDIR *source_dir;\n> +\tstruct dirent *d;\n> +\n> +\tmkdir(dest, dest_mode);\n> +\n> +\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n> +\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n> +\t\tchar *child;\n> +\n> +\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n> +\t\t\tcontinue;\n> +\n> +\t\tchild = joinpath(source, d->d_name, -1);\n> +\t\tfs_copy(child, dest);\n> +\t\tfree(child);\n> +\t}\n> +\n> +\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n> +\n> +\tclosedir(source_dir);\n> +}\n> +\n> +static void\n> +fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n> +{\n> +\tint in, out;\n> +\n> +\tcl_must_pass((in = open(source, O_RDONLY)));\n> +\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n> +\n> +#if USE_FCOPYFILE && defined(__APPLE__)\n> +\t((void)(source_len)); /* unused */\n> +\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n> +#elif USE_SENDFILE && defined(__linux__)\n> +\t{\n> +\t\tssize_t ret = 0;\n> +\n> +\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n> +\t\t\tsource_len -= (size_t)ret;\n> +\t\t}\n> +\t\tcl_assert(ret >= 0);\n> +\t}\n> +#else\n> +\t{\n> +\t\tchar buf[131072];\n> +\t\tssize_t ret;\n> +\n> +\t\t((void)(source_len)); /* unused */\n> +\n> +\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n> +\t\t\tsize_t len = (size_t)ret;\n> +\n> +\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n> +\t\t\t\tcl_assert(ret <= (ssize_t)len);\n> +\t\t\t\tlen -= ret;\n> +\t\t\t}\n> +\t\t\tcl_assert(ret >= 0);\n> +\t\t}\n> +\t\tcl_assert(ret == 0);\n> +\t}\n> +#endif\n> +\n> +\tclose(in);\n> +\tclose(out);\n> +}\n> +\n> +static void\n> +fs_copy(const char *source, const char *_dest)\n> +{\n> +\tchar *dbuf = NULL;\n> +\tconst char *dest = NULL;\n> +\tstruct stat source_st, dest_st;\n> +\n> +\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n> +\n> +\tif (lstat(_dest, &dest_st) == 0) {\n> +\t\tconst char *base;\n> +\t\tint base_len;\n> +\n> +\t\t/* Target exists and is directory; append basename */\n> +\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n> +\n> +\t\tbasename_r(&base, &base_len, source);\n> +\t\tcl_assert(base_len < INT_MAX);\n> +\n> +\t\tdbuf = joinpath(_dest, base, base_len);\n> +\t\tdest = dbuf;\n> +\t} else if (errno != ENOENT) {\n> +\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n> +\t} else {\n> +\t\tdest = _dest;\n> +\t}\n> +\n> +\tif (S_ISDIR(source_st.st_mode)) {\n> +\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n> +\t} else {\n> +\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n> +\t}\n> +\n> +\tfree(dbuf);\n> +}\n> +\n> +static void\n> +fs_rmdir_helper(const char *path)\n> +{\n> +\tDIR *dir;\n> +\tstruct dirent *d;\n> +\n> +\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n> +\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n> +\t\tchar *child;\n> +\n> +\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n> +\t\t\tcontinue;\n> +\n> +\t\tchild = joinpath(path, d->d_name, -1);\n> +\t\tfs_rm(child);\n> +\t\tfree(child);\n> +\t}\n> +\n> +\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n> +\tclosedir(dir);\n> +\n> +\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n> +}\n> +\n> +static void\n> +fs_rm(const char *path)\n> +{\n> +\tstruct stat st;\n> +\n> +\tif (lstat(path, &st)) {\n> +\t\tif (errno == ENOENT)\n> +\t\t\treturn;\n> +\n> +\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n> +\t}\n> +\n> +\tif (S_ISDIR(st.st_mode)) {\n> +\t\tfs_rmdir_helper(path);\n> +\t} else {\n> +\t\tcl_must_pass(unlink(path));\n> +\t}\n> +}\n> +\n> +void\n> +cl_fs_cleanup(void)\n> +{\n> +\tclar_unsandbox();\n> +\tclar_sandbox();\n> +}\n> +#endif\n> diff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\n> new file mode 100644\n> index 0000000000..c17e2f693b\n> --- /dev/null\n> +++ b/t/unit-tests/clar/clar/print.h\n> @@ -0,0 +1,211 @@\n> +/* clap: clar protocol, the traditional clar output format */\n> +\n> +static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n> +{\n> +\t(void)test_count;\n> +\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n> +\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n> +}\n> +\n> +static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n> +{\n> +\t(void)test_count;\n> +\t(void)suite_count;\n> +\t(void)error_count;\n> +\n> +\tprintf(\"\\n\\n\");\n> +\tclar_report_all();\n> +}\n> +\n> +static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n> +{\n> +\tprintf(\"  %d) Failure:\\n\", num);\n> +\n> +\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n> +\t\treport->suite,\n> +\t\treport->test,\n> +\t\terror->file,\n> +\t\terror->line_number);\n> +\n> +\tprintf(\"  %s\\n\", error->error_msg);\n> +\n> +\tif (error->description != NULL)\n> +\t\tprintf(\"  %s\\n\", error->description);\n> +\n> +\tprintf(\"\\n\");\n> +\tfflush(stdout);\n> +}\n> +\n> +static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n> +{\n> +\t(void)test_name;\n> +\t(void)test_number;\n> +\n> +\tif (_clar.verbosity > 1) {\n> +\t\tprintf(\"%s::%s: \", suite_name, test_name);\n> +\n> +\t\tswitch (status) {\n> +\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n> +\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n> +\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n> +\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n> +\t\t}\n> +\t} else {\n> +\t\tswitch (status) {\n> +\t\tcase CL_TEST_OK: printf(\".\"); break;\n> +\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n> +\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n> +\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n> +\t\t}\n> +\n> +\t\tfflush(stdout);\n> +\t}\n> +}\n> +\n> +static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n> +{\n> +\tif (_clar.verbosity == 1)\n> +\t\tprintf(\"\\n%s\", suite_name);\n> +\n> +\t(void)suite_index;\n> +}\n> +\n> +static void clar_print_clap_onabort(const char *fmt, va_list arg)\n> +{\n> +\tvfprintf(stderr, fmt, arg);\n> +}\n> +\n> +/* tap: test anywhere protocol format */\n> +\n> +static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n> +{\n> +\t(void)test_count;\n> +\t(void)suite_count;\n> +\t(void)suite_names;\n> +\tprintf(\"TAP version 13\\n\");\n> +}\n> +\n> +static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n> +{\n> +\t(void)suite_count;\n> +\t(void)error_count;\n> +\n> +\tprintf(\"1..%d\\n\", test_count);\n> +}\n> +\n> +static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n> +{\n> +\t(void)num;\n> +\t(void)report;\n> +\t(void)error;\n> +}\n> +\n> +static void print_escaped(const char *str)\n> +{\n> +\tchar *c;\n> +\n> +\twhile ((c = strchr(str, '\\'')) != NULL) {\n> +\t\tprintf(\"%.*s\", (int)(c - str), str);\n> +\t\tprintf(\"''\");\n> +\t\tstr = c + 1;\n> +\t}\n> +\n> +\tprintf(\"%s\", str);\n> +}\n> +\n> +static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n> +{\n> +\tconst struct clar_error *error = _clar.last_report->errors;\n> +\n> +\t(void)test_name;\n> +\t(void)test_number;\n> +\n> +\tswitch(status) {\n> +\tcase CL_TEST_OK:\n> +\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n> +\t\tbreak;\n> +\tcase CL_TEST_FAILURE:\n> +\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n> +\n> +\t\tprintf(\"    ---\\n\");\n> +\t\tprintf(\"    reason: |\\n\");\n> +\t\tprintf(\"      %s\\n\", error->error_msg);\n> +\n> +\t\tif (error->description)\n> +\t\t\tprintf(\"      %s\\n\", error->description);\n> +\n> +\t\tprintf(\"    at:\\n\");\n> +\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n> +\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n> +\t\tprintf(\"      function: '%s'\\n\", error->function);\n> +\t\tprintf(\"    ---\\n\");\n> +\n> +\t\tbreak;\n> +\tcase CL_TEST_SKIP:\n> +\tcase CL_TEST_NOTRUN:\n> +\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n> +\t\tbreak;\n> +\t}\n> +\n> +\tfflush(stdout);\n> +}\n> +\n> +static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n> +{\n> +\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n> +}\n> +\n> +static void clar_print_tap_onabort(const char *fmt, va_list arg)\n> +{\n> +\tprintf(\"Bail out! \");\n> +\tvprintf(fmt, arg);\n> +\tfflush(stdout);\n> +}\n> +\n> +/* indirection between protocol output selection */\n> +\n> +#define PRINT(FN, ...) do { \\\n> +\t\tswitch (_clar.output_format) { \\\n> +\t\t\tcase CL_OUTPUT_CLAP: \\\n> +\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n> +\t\t\t\tbreak; \\\n> +\t\t\tcase CL_OUTPUT_TAP: \\\n> +\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n> +\t\t\t\tbreak; \\\n> +\t\t\tdefault: \\\n> +\t\t\t\tabort(); \\\n> +\t\t} \\\n> +\t} while (0)\n> +\n> +static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n> +{\n> +\tPRINT(init, test_count, suite_count, suite_names);\n> +}\n> +\n> +static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n> +{\n> +\tPRINT(shutdown, test_count, suite_count, error_count);\n> +}\n> +\n> +static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n> +{\n> +\tPRINT(error, num, report, error);\n> +}\n> +\n> +static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n> +{\n> +\tPRINT(ontest, suite_name, test_name, test_number, status);\n> +}\n> +\n> +static void clar_print_onsuite(const char *suite_name, int suite_index)\n> +{\n> +\tPRINT(onsuite, suite_name, suite_index);\n> +}\n> +\n> +static void clar_print_onabort(const char *msg, ...)\n> +{\n> +\tva_list argp;\n> +\tva_start(argp, msg);\n> +\tPRINT(onabort, msg, argp);\n> +\tva_end(argp);\n> +}\n> diff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\n> new file mode 100644\n> index 0000000000..7c177f3525\n> --- /dev/null\n> +++ b/t/unit-tests/clar/clar/sandbox.h\n> @@ -0,0 +1,153 @@\n> +#ifdef __APPLE__\n> +#include <sys/syslimits.h>\n> +#endif\n> +\n> +static char _clar_path[4096 + 1];\n> +\n> +static int\n> +is_valid_tmp_path(const char *path)\n> +{\n> +\tSTAT_T st;\n> +\n> +\tif (stat(path, &st) != 0)\n> +\t\treturn 0;\n> +\n> +\tif (!S_ISDIR(st.st_mode))\n> +\t\treturn 0;\n> +\n> +\treturn (access(path, W_OK) == 0);\n> +}\n> +\n> +static int\n> +find_tmp_path(char *buffer, size_t length)\n> +{\n> +#ifndef _WIN32\n> +\tstatic const size_t var_count = 5;\n> +\tstatic const char *env_vars[] = {\n> +\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n> +\t};\n> +\n> +\tsize_t i;\n> +\n> +\tfor (i = 0; i < var_count; ++i) {\n> +\t\tconst char *env = getenv(env_vars[i]);\n> +\t\tif (!env)\n> +\t\t\tcontinue;\n> +\n> +\t\tif (is_valid_tmp_path(env)) {\n> +#ifdef __APPLE__\n> +\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n> +\t\t\t\treturn 0;\n> +#endif\n> +\t\t\tstrncpy(buffer, env, length - 1);\n> +\t\t\tbuffer[length - 1] = '\\0';\n> +\t\t\treturn 0;\n> +\t\t}\n> +\t}\n> +\n> +\t/* If the environment doesn't say anything, try to use /tmp */\n> +\tif (is_valid_tmp_path(\"/tmp\")) {\n> +#ifdef __APPLE__\n> +\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n> +\t\t\treturn 0;\n> +#endif\n> +\t\tstrncpy(buffer, \"/tmp\", length - 1);\n> +\t\tbuffer[length - 1] = '\\0';\n> +\t\treturn 0;\n> +\t}\n> +\n> +#else\n> +\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n> +\tif (env_len > 0 && env_len < (DWORD)length)\n> +\t\treturn 0;\n> +\n> +\tif (GetTempPath((DWORD)length, buffer))\n> +\t\treturn 0;\n> +#endif\n> +\n> +\t/* This system doesn't like us, try to use the current directory */\n> +\tif (is_valid_tmp_path(\".\")) {\n> +\t\tstrncpy(buffer, \".\", length - 1);\n> +\t\tbuffer[length - 1] = '\\0';\n> +\t\treturn 0;\n> +\t}\n> +\n> +\treturn -1;\n> +}\n> +\n> +static void clar_unsandbox(void)\n> +{\n> +\tif (_clar_path[0] == '\\0')\n> +\t\treturn;\n> +\n> +\tcl_must_pass(chdir(\"..\"));\n> +\n> +\tfs_rm(_clar_path);\n> +}\n> +\n> +static int build_sandbox_path(void)\n> +{\n> +#ifdef CLAR_TMPDIR\n> +\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n> +#else\n> +\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n> +#endif\n> +\n> +\tsize_t len;\n> +\n> +\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n> +\t\treturn -1;\n> +\n> +\tlen = strlen(_clar_path);\n> +\n> +#ifdef _WIN32\n> +\t{ /* normalize path to POSIX forward slashes */\n> +\t\tsize_t i;\n> +\t\tfor (i = 0; i < len; ++i) {\n> +\t\t\tif (_clar_path[i] == '\\\\')\n> +\t\t\t\t_clar_path[i] = '/';\n> +\t\t}\n> +\t}\n> +#endif\n> +\n> +\tif (_clar_path[len - 1] != '/') {\n> +\t\t_clar_path[len++] = '/';\n> +\t}\n> +\n> +\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n> +\n> +#if defined(__MINGW32__)\n> +\tif (_mktemp(_clar_path) == NULL)\n> +\t\treturn -1;\n> +\n> +\tif (mkdir(_clar_path, 0700) != 0)\n> +\t\treturn -1;\n> +#elif defined(_WIN32)\n> +\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n> +\t\treturn -1;\n> +\n> +\tif (mkdir(_clar_path, 0700) != 0)\n> +\t\treturn -1;\n> +#else\n> +\tif (mkdtemp(_clar_path) == NULL)\n> +\t\treturn -1;\n> +#endif\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int clar_sandbox(void)\n> +{\n> +\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n> +\t\treturn -1;\n> +\n> +\tif (chdir(_clar_path) != 0)\n> +\t\treturn -1;\n> +\n> +\treturn 0;\n> +}\n> +\n> +const char *clar_sandbox_path(void)\n> +{\n> +\treturn _clar_path;\n> +}\n> diff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\n> new file mode 100644\n> index 0000000000..4dd352e28b\n> --- /dev/null\n> +++ b/t/unit-tests/clar/clar/summary.h\n> @@ -0,0 +1,143 @@\n> +\n> +#include <stdio.h>\n> +#include <time.h>\n> +\n> +static int clar_summary_close_tag(\n> +    struct clar_summary *summary, const char *tag, int indent)\n> +{\n> +\tconst char *indt;\n> +\n> +\tif (indent == 0) indt = \"\";\n> +\telse if (indent == 1) indt = \"\\t\";\n> +\telse indt = \"\\t\\t\";\n> +\n> +\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n> +}\n> +\n> +static int clar_summary_testsuites(struct clar_summary *summary)\n> +{\n> +\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n> +}\n> +\n> +static int clar_summary_testsuite(struct clar_summary *summary,\n> +    int idn, const char *name, time_t timestamp,\n> +    int test_count, int fail_count, int error_count)\n> +{\n> +\tstruct tm *tm = localtime(&timestamp);\n> +\tchar iso_dt[20];\n> +\n> +\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n> +\t\treturn -1;\n> +\n> +\treturn fprintf(summary->fp, \"\\t<testsuite\"\n> +\t\t       \" id=\\\"%d\\\"\"\n> +\t\t       \" name=\\\"%s\\\"\"\n> +\t\t       \" hostname=\\\"localhost\\\"\"\n> +\t\t       \" timestamp=\\\"%s\\\"\"\n> +\t\t       \" tests=\\\"%d\\\"\"\n> +\t\t       \" failures=\\\"%d\\\"\"\n> +\t\t       \" errors=\\\"%d\\\">\\n\",\n> +\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n> +}\n> +\n> +static int clar_summary_testcase(struct clar_summary *summary,\n> +    const char *name, const char *classname, double elapsed)\n> +{\n> +\treturn fprintf(summary->fp,\n> +\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n> +\t\tname, classname, elapsed);\n> +}\n> +\n> +static int clar_summary_failure(struct clar_summary *summary,\n> +    const char *type, const char *message, const char *desc)\n> +{\n> +\treturn fprintf(summary->fp,\n> +\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n> +\t    type, message, desc);\n> +}\n> +\n> +static int clar_summary_skipped(struct clar_summary *summary)\n> +{\n> +\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n> +}\n> +\n> +struct clar_summary *clar_summary_init(const char *filename)\n> +{\n> +\tstruct clar_summary *summary;\n> +\tFILE *fp;\n> +\n> +\tif ((fp = fopen(filename, \"w\")) == NULL) {\n> +\t\tperror(\"fopen\");\n> +\t\treturn NULL;\n> +\t}\n> +\n> +\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n> +\t\tperror(\"malloc\");\n> +\t\tfclose(fp);\n> +\t\treturn NULL;\n> +\t}\n> +\n> +\tsummary->filename = filename;\n> +\tsummary->fp = fp;\n> +\n> +\treturn summary;\n> +}\n> +\n> +int clar_summary_shutdown(struct clar_summary *summary)\n> +{\n> +\tstruct clar_report *report;\n> +\tconst char *last_suite = NULL;\n> +\n> +\tif (clar_summary_testsuites(summary) < 0)\n> +\t\tgoto on_error;\n> +\n> +\treport = _clar.reports;\n> +\twhile (report != NULL) {\n> +\t\tstruct clar_error *error = report->errors;\n> +\n> +\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n> +\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n> +\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n> +\t\t\t\tgoto on_error;\n> +\t\t}\n> +\n> +\t\tlast_suite = report->suite;\n> +\n> +\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n> +\n> +\t\twhile (error != NULL) {\n> +\t\t\tif (clar_summary_failure(summary, \"assert\",\n> +\t\t\t    error->error_msg, error->description) < 0)\n> +\t\t\t\tgoto on_error;\n> +\n> +\t\t\terror = error->next;\n> +\t\t}\n> +\n> +\t\tif (report->status == CL_TEST_SKIP)\n> +\t\t\tclar_summary_skipped(summary);\n> +\n> +\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n> +\t\t\tgoto on_error;\n> +\n> +\t\treport = report->next;\n> +\n> +\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n> +\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n> +\t\t\t\tgoto on_error;\n> +\t\t}\n> +\t}\n> +\n> +\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n> +\t    fclose(summary->fp) != 0)\n> +\t\tgoto on_error;\n> +\n> +\tprintf(\"written summary file to %s\\n\", summary->filename);\n> +\n> +\tfree(summary);\n> +\treturn 0;\n> +\n> +on_error:\n> +\tfclose(summary->fp);\n> +\tfree(summary);\n> +\treturn -1;\n> +}\n> diff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\n> new file mode 100755\n> index 0000000000..80996ac3e7\n> --- /dev/null\n> +++ b/t/unit-tests/clar/generate.py\n> @@ -0,0 +1,266 @@\n> +#!/usr/bin/env python\n> +#\n> +# Copyright (c) Vicent Marti. All rights reserved.\n> +#\n> +# This file is part of clar, distributed under the ISC license.\n> +# For full terms see the included COPYING file.\n> +#\n> +\n> +from __future__ import with_statement\n> +from string import Template\n> +import re, fnmatch, os, sys, codecs, pickle\n> +\n> +class Module(object):\n> +    class Template(object):\n> +        def __init__(self, module):\n> +            self.module = module\n> +\n> +        def _render_callback(self, cb):\n> +            if not cb:\n> +                return '    { NULL, NULL }'\n> +            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n> +\n> +    class DeclarationTemplate(Template):\n> +        def render(self):\n> +            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n> +\n> +            for initializer in self.module.initializers:\n> +                out += \"extern %s;\\n\" % initializer['declaration']\n> +\n> +            if self.module.cleanup:\n> +                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n> +\n> +            return out\n> +\n> +    class CallbacksTemplate(Template):\n> +        def render(self):\n> +            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n> +            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n> +            out += \"\\n};\\n\"\n> +            return out\n> +\n> +    class InfoTemplate(Template):\n> +        def render(self):\n> +            templates = []\n> +\n> +            initializers = self.module.initializers\n> +            if len(initializers) == 0:\n> +                initializers = [ None ]\n> +\n> +            for initializer in initializers:\n> +                name = self.module.clean_name()\n> +                if initializer and initializer['short_name'].startswith('initialize_'):\n> +                    variant = initializer['short_name'][len('initialize_'):]\n> +                    name += \" (%s)\" % variant.replace('_', ' ')\n> +\n> +                template = Template(\n> +            r\"\"\"\n> +    {\n> +        \"${clean_name}\",\n> +    ${initialize},\n> +    ${cleanup},\n> +        ${cb_ptr}, ${cb_count}, ${enabled}\n> +    }\"\"\"\n> +                ).substitute(\n> +                    clean_name = name,\n> +                    initialize = self._render_callback(initializer),\n> +                    cleanup = self._render_callback(self.module.cleanup),\n> +                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n> +                    cb_count = len(self.module.callbacks),\n> +                    enabled = int(self.module.enabled)\n> +                )\n> +                templates.append(template)\n> +\n> +            return ','.join(templates)\n> +\n> +    def __init__(self, name):\n> +        self.name = name\n> +\n> +        self.mtime = None\n> +        self.enabled = True\n> +        self.modified = False\n> +\n> +    def clean_name(self):\n> +        return self.name.replace(\"_\", \"::\")\n> +\n> +    def _skip_comments(self, text):\n> +        SKIP_COMMENTS_REGEX = re.compile(\n> +            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n> +            re.DOTALL | re.MULTILINE)\n> +\n> +        def _replacer(match):\n> +            s = match.group(0)\n> +            return \"\" if s.startswith('/') else s\n> +\n> +        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n> +\n> +    def parse(self, contents):\n> +        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n> +\n> +        contents = self._skip_comments(contents)\n> +        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n> +\n> +        self.callbacks = []\n> +        self.initializers = []\n> +        self.cleanup = None\n> +\n> +        for (declaration, symbol, short_name) in regex.findall(contents):\n> +            data = {\n> +                \"short_name\" : short_name,\n> +                \"declaration\" : declaration,\n> +                \"symbol\" : symbol\n> +            }\n> +\n> +            if short_name.startswith('initialize'):\n> +                self.initializers.append(data)\n> +            elif short_name == 'cleanup':\n> +                self.cleanup = data\n> +            else:\n> +                self.callbacks.append(data)\n> +\n> +        return self.callbacks != []\n> +\n> +    def refresh(self, path):\n> +        self.modified = False\n> +\n> +        try:\n> +            st = os.stat(path)\n> +\n> +            # Not modified\n> +            if st.st_mtime == self.mtime:\n> +                return True\n> +\n> +            self.modified = True\n> +            self.mtime = st.st_mtime\n> +\n> +            with codecs.open(path, encoding='utf-8') as fp:\n> +                raw_content = fp.read()\n> +\n> +        except IOError:\n> +            return False\n> +\n> +        return self.parse(raw_content)\n> +\n> +class TestSuite(object):\n> +\n> +    def __init__(self, path, output):\n> +        self.path = path\n> +        self.output = output\n> +\n> +    def should_generate(self, path):\n> +        if not os.path.isfile(path):\n> +            return True\n> +\n> +        if any(module.modified for module in self.modules.values()):\n> +            return True\n> +\n> +        return False\n> +\n> +    def find_modules(self):\n> +        modules = []\n> +        for root, _, files in os.walk(self.path):\n> +            module_root = root[len(self.path):]\n> +            module_root = [c for c in module_root.split(os.sep) if c]\n> +\n> +            tests_in_module = fnmatch.filter(files, \"*.c\")\n> +\n> +            for test_file in tests_in_module:\n> +                full_path = os.path.join(root, test_file)\n> +                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n> +\n> +                modules.append((full_path, module_name))\n> +\n> +        return modules\n> +\n> +    def load_cache(self):\n> +        path = os.path.join(self.output, '.clarcache')\n> +        cache = {}\n> +\n> +        try:\n> +            fp = open(path, 'rb')\n> +            cache = pickle.load(fp)\n> +            fp.close()\n> +        except (IOError, ValueError):\n> +            pass\n> +\n> +        return cache\n> +\n> +    def save_cache(self):\n> +        path = os.path.join(self.output, '.clarcache')\n> +        with open(path, 'wb') as cache:\n> +            pickle.dump(self.modules, cache)\n> +\n> +    def load(self, force = False):\n> +        module_data = self.find_modules()\n> +        self.modules = {} if force else self.load_cache()\n> +\n> +        for path, name in module_data:\n> +            if name not in self.modules:\n> +                self.modules[name] = Module(name)\n> +\n> +            if not self.modules[name].refresh(path):\n> +                del self.modules[name]\n> +\n> +    def disable(self, excluded):\n> +        for exclude in excluded:\n> +            for module in self.modules.values():\n> +                name = module.clean_name()\n> +                if name.startswith(exclude):\n> +                    module.enabled = False\n> +                    module.modified = True\n> +\n> +    def suite_count(self):\n> +        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n> +\n> +    def callback_count(self):\n> +        return sum(len(module.callbacks) for module in self.modules.values())\n> +\n> +    def write(self):\n> +        output = os.path.join(self.output, 'clar.suite')\n> +\n> +        if not self.should_generate(output):\n> +            return False\n> +\n> +        with open(output, 'w') as data:\n> +            modules = sorted(self.modules.values(), key=lambda module: module.name)\n> +\n> +            for module in modules:\n> +                t = Module.DeclarationTemplate(module)\n> +                data.write(t.render())\n> +\n> +            for module in modules:\n> +                t = Module.CallbacksTemplate(module)\n> +                data.write(t.render())\n> +\n> +            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n> +                Module.InfoTemplate(module).render() for module in modules\n> +            ) + \"\\n};\\n\"\n> +\n> +            data.write(suites)\n> +\n> +            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n> +            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n> +\n> +        self.save_cache()\n> +        return True\n> +\n> +if __name__ == '__main__':\n> +    from optparse import OptionParser\n> +\n> +    parser = OptionParser()\n> +    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n> +    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n> +    parser.add_option('-o', '--output', dest='output')\n> +\n> +    options, args = parser.parse_args()\n> +    if len(args) > 1:\n> +        print(\"More than one path given\")\n> +        sys.exit(1)\n> +\n> +    path = args.pop() if args else '.'\n> +    output = options.output or path\n> +    suite = TestSuite(path, output)\n> +    suite.load(options.force)\n> +    suite.disable(options.excluded)\n> +    if suite.write():\n> +        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\n> diff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\n> new file mode 100644\n> index 0000000000..a477d0c40c\n> --- /dev/null\n> +++ b/t/unit-tests/clar/test/.gitignore\n> @@ -0,0 +1,4 @@\n> +clar.suite\n> +.clarcache\n> +clar_test\n> +*.o\n> diff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\n> new file mode 100644\n> index 0000000000..93c6b2ad32\n> --- /dev/null\n> +++ b/t/unit-tests/clar/test/Makefile\n> @@ -0,0 +1,39 @@\n> +#\n> +# Copyright (c) Vicent Marti. All rights reserved.\n> +#\n> +# This file is part of clar, distributed under the ISC license.\n> +# For full terms see the included COPYING file.\n> +#\n> +\n> +#\n> +# Set up the path to the clar sources and to the fixtures directory\n> +#\n> +# The fixture path needs to be an absolute path so it can be used\n> +# even after we have chdir'ed into the test directory while testing.\n> +#\n> +CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n> +TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n> +CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n> +CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n> +\n> +CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n> +\n> +.PHONY: clean\n> +\n> +# list the objects that go into our test\n> +objects = main.o sample.o\n> +\n> +# build the test executable itself\n> +clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n> +\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n> +\n> +# test object files depend on clar macros\n> +$(objects) : $(CLAR_PATH)clar.h\n> +\n> +# build the clar.suite file of test metadata\n> +clar.suite:\n> +\tpython \"$(CLAR_PATH)generate.py\" .\n> +\n> +# remove all generated files\n> +clean:\n> +\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\n> diff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\n> new file mode 100644\n> index 0000000000..0fcaa639aa\n> --- /dev/null\n> +++ b/t/unit-tests/clar/test/clar_test.h\n> @@ -0,0 +1,16 @@\n> +/*\n> + * Copyright (c) Vicent Marti. All rights reserved.\n> + *\n> + * This file is part of clar, distributed under the ISC license.\n> + * For full terms see the included COPYING file.\n> + */\n> +#ifndef __CLAR_TEST__\n> +#define __CLAR_TEST__\n> +\n> +/* Import the standard clar helper functions */\n> +#include \"clar.h\"\n> +\n> +/* Your custom shared includes / defines here */\n> +extern int global_test_counter;\n> +\n> +#endif\n> diff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\n> new file mode 100644\n> index 0000000000..59e56ad255\n> --- /dev/null\n> +++ b/t/unit-tests/clar/test/main.c\n> @@ -0,0 +1,40 @@\n> +/*\n> + * Copyright (c) Vicent Marti. All rights reserved.\n> + *\n> + * This file is part of clar, distributed under the ISC license.\n> + * For full terms see the included COPYING file.\n> + */\n> +\n> +#include \"clar_test.h\"\n> +\n> +/*\n> + * Sample main() for clar tests.\n> + *\n> + * You should write your own main routine for clar tests that does specific\n> + * setup and teardown as necessary for your application.  The only required\n> + * line is the call to `clar_test(argc, argv)`, which will execute the test\n> + * suite.  If you want to check the return value of the test application,\n> + * your main() should return the same value returned by clar_test().\n> + */\n> +\n> +int global_test_counter = 0;\n> +\n> +#ifdef _WIN32\n> +int __cdecl main(int argc, char *argv[])\n> +#else\n> +int main(int argc, char *argv[])\n> +#endif\n> +{\n> +\tint ret;\n> +\n> +\t/* Your custom initialization here */\n> +\tglobal_test_counter = 0;\n> +\n> +\t/* Run the test suite */\n> +\tret = clar_test(argc, argv);\n> +\n> +\t/* Your custom cleanup here */\n> +\tcl_assert_equal_i(8, global_test_counter);\n> +\n> +\treturn ret;\n> +}\n> diff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\n> new file mode 100644\n> index 0000000000..a4d91b72fa\n> --- /dev/null\n> +++ b/t/unit-tests/clar/test/main.c.sample\n> @@ -0,0 +1,27 @@\n> +/*\n> + * Copyright (c) Vicent Marti. All rights reserved.\n> + *\n> + * This file is part of clar, distributed under the ISC license.\n> + * For full terms see the included COPYING file.\n> + */\n> +\n> +#include \"clar_test.h\"\n> +\n> +/*\n> + * Minimal main() for clar tests.\n> + *\n> + * Modify this with any application specific setup or teardown that you need.\n> + * The only required line is the call to `clar_test(argc, argv)`, which will\n> + * execute the test suite.  If you want to check the return value of the test\n> + * application, main() should return the same value returned by clar_test().\n> + */\n> +\n> +#ifdef _WIN32\n> +int __cdecl main(int argc, char *argv[])\n> +#else\n> +int main(int argc, char *argv[])\n> +#endif\n> +{\n> +\t/* Run the test suite */\n> +\treturn clar_test(argc, argv);\n> +}\n> diff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\n> new file mode 100644\n> index 0000000000..220f4aa98a\n> --- /dev/null\n> +++ b/t/unit-tests/clar/test/resources/test/file\n> @@ -0,0 +1 @@\n> +File\n> diff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\n> new file mode 100644\n> index 0000000000..faa1209262\n> --- /dev/null\n> +++ b/t/unit-tests/clar/test/sample.c\n> @@ -0,0 +1,84 @@\n> +#include \"clar_test.h\"\n> +#include <sys/stat.h>\n> +\n> +static int file_size(const char *filename)\n> +{\n> +\tstruct stat st;\n> +\n> +\tif (stat(filename, &st) == 0)\n> +\t\treturn (int)st.st_size;\n> +\treturn -1;\n> +}\n> +\n> +void test_sample__initialize(void)\n> +{\n> +\tglobal_test_counter++;\n> +}\n> +\n> +void test_sample__cleanup(void)\n> +{\n> +\tcl_fixture_cleanup(\"test\");\n> +\n> +\tcl_assert(file_size(\"test/file\") == -1);\n> +}\n> +\n> +void test_sample__1(void)\n> +{\n> +\tcl_assert(1);\n> +\tcl_must_pass(0);  /* 0 == success */\n> +\tcl_must_fail(-1); /* <0 == failure */\n> +\tcl_must_pass(-1); /* demonstrate a failing call */\n> +}\n> +\n> +void test_sample__2(void)\n> +{\n> +\tcl_fixture_sandbox(\"test\");\n> +\n> +\tcl_assert(file_size(\"test/nonexistent\") == -1);\n> +\tcl_assert(file_size(\"test/file\") > 0);\n> +\tcl_assert(100 == 101);\n> +}\n> +\n> +void test_sample__strings(void)\n> +{\n> +\tconst char *actual = \"expected\";\n> +\tcl_assert_equal_s(\"expected\", actual);\n> +\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n> +\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n> +}\n> +\n> +void test_sample__strings_with_length(void)\n> +{\n> +\tconst char *actual = \"expected\";\n> +\tcl_assert_equal_strn(\"expected_\", actual, 8);\n> +\tcl_assert_equal_strn(\"exactly\", actual, 2);\n> +\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n> +\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n> +}\n> +\n> +void test_sample__int(void)\n> +{\n> +\tint value = 100;\n> +\tcl_assert_equal_i(100, value);\n> +\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n> +}\n> +\n> +void test_sample__int_fmt(void)\n> +{\n> +\tint value = 100;\n> +\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n> +}\n> +\n> +void test_sample__bool(void)\n> +{\n> +\tint value = 100;\n> +\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n> +\tcl_assert_equal_b(0, value);\n> +}\n> +\n> +void test_sample__ptr(void)\n> +{\n> +\tconst char *actual = \"expected\";\n> +\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n> +\tcl_assert_equal_p(&actual, actual);\n> +}\n"},{"id":"501139","messageId":"1a38638e-561b-4e97-a462-f5ff034832e1@gmail.com","threadId":"61874","inReplyTo":"8f56b4d6264b8e5a85ba73bc8ac541efa066c8a6.1723791831.git.ps@pks.im","subject":"Re: [PATCH v5 8/9] t/unit-tests: convert strvec tests to use clar","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-16T13:38:22Z","receivedAt":"2024-08-16T13:38:25Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 16/08/2024 08:04, Patrick Steinhardt wrote:\n> diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\n> index 32a81299e9..82b7635e6a 100644\n> --- a/t/unit-tests/unit-test.c\n> +++ b/t/unit-tests/unit-test.c\n> @@ -6,10 +6,9 @@ int cmd_main(int argc, const char **argv)\n>   \tint ret;\n>   \n>   \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n> -\tALLOC_ARRAY(argv_copy, argc + 2);\n> +\tALLOC_ARRAY(argv_copy, argc + 1);\n>   \tCOPY_ARRAY(argv_copy, argv, argc);\n>   \targv_copy[argc++] = \"-t\";\n> -\targv_copy[argc] = NULL;\n\nI'm confused by this - it looks like it belongs in the previous patch \nbut why are we deleting the line that adds the terminating NULL to argv \nin the first place?\n\nThanks\n\nPhillip\n"},{"id":"501140","messageId":"4aa6f557-1dfc-4aaa-b6db-e47013568ecc@gmail.com","threadId":"61874","inReplyTo":"ca09d19fd51cd4b3072b339f483b6b6d6e467b56.1723791831.git.ps@pks.im","subject":"Re: [PATCH v5 9/9] t/unit-tests: convert ctype tests to use clar","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-16T13:38:30Z","receivedAt":"2024-08-16T13:38:33Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 16/08/2024 08:05, Patrick Steinhardt wrote:\n>   #define TEST_CHAR_CLASS(class, string) do { \\\n>   \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n>   \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n>   \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n> -\tint skip = test__run_begin(); \\\n> -\tif (!skip) { \\\n> -\t\tfor (int i = 0; i < 256; i++) { \\\n> -\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n> -\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n> -\t\t} \\\n> -\t\tcheck(!class(EOF)); \\\n> -\t} \\\n> -\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n> +\tfor (int i = 0; i < 256; i++) \\\n> +\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n\nIf this fails how are we supposed to know which character it was checking?\n\nThanks\n\nPhillip\n\n> +\tcl_assert(!class(EOF)); \\\n>   } while (0)\n>   \n>   #define DIGIT \"0123456789\"\n> @@ -33,21 +27,72 @@\n>   \t\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\" \\\n>   \t\"\\x7f\"\n>   \n> -int cmd_main(int argc, const char **argv) {\n> +void test_ctype__isspace(void)\n> +{\n>   \tTEST_CHAR_CLASS(isspace, \" \\n\\r\\t\");\n> +}\n> +\n> +void test_ctype__isdigit(void)\n> +{\n>   \tTEST_CHAR_CLASS(isdigit, DIGIT);\n> +}\n> +\n> +void test_ctype__isalpha(void)\n> +{\n>   \tTEST_CHAR_CLASS(isalpha, LOWER UPPER);\n> +}\n> +\n> +void test_ctype__isalnum(void)\n> +{\n>   \tTEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);\n> +}\n> +\n> +void test_ctype__is_glob_special(void)\n> +{\n>   \tTEST_CHAR_CLASS(is_glob_special, \"*?[\\\\\");\n> +}\n> +\n> +void test_ctype__is_regex_special(void)\n> +{\n>   \tTEST_CHAR_CLASS(is_regex_special, \"$()*+.?[\\\\^{|\");\n> +}\n> +\n> +void test_ctype__is_pathspec_magic(void)\n> +{\n>   \tTEST_CHAR_CLASS(is_pathspec_magic, \"!\\\"#%&',-/:;<=>@_`~\");\n> +}\n> +\n> +void test_ctype__isascii(void)\n> +{\n>   \tTEST_CHAR_CLASS(isascii, ASCII);\n> +}\n> +\n> +void test_ctype__islower(void)\n> +{\n>   \tTEST_CHAR_CLASS(islower, LOWER);\n> +}\n> +\n> +void test_ctype__isupper(void)\n> +{\n>   \tTEST_CHAR_CLASS(isupper, UPPER);\n> +}\n> +\n> +void test_ctype__iscntrl(void)\n> +{\n>   \tTEST_CHAR_CLASS(iscntrl, CNTRL);\n> +}\n> +\n> +void test_ctype__ispunct(void)\n> +{\n>   \tTEST_CHAR_CLASS(ispunct, PUNCT);\n> +}\n> +\n> +void test_ctype__isxdigit(void)\n> +{\n>   \tTEST_CHAR_CLASS(isxdigit, DIGIT \"abcdefABCDEF\");\n> -\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n> +}\n>   \n> -\treturn test_done();\n> +void test_ctype__isprint(void)\n> +{\n> +\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n>   }\n"},{"id":"501142","messageId":"xmqq8qwwmoqv.fsf@gitster.g","threadId":"61874","inReplyTo":"1a38638e-561b-4e97-a462-f5ff034832e1@gmail.com","subject":"Re: [PATCH v5 8/9] t/unit-tests: convert strvec tests to use clar","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-16T16:11:36Z","receivedAt":"2024-08-16T16:11:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Hi Patrick\n>\n> On 16/08/2024 08:04, Patrick Steinhardt wrote:\n>> diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\n>> index 32a81299e9..82b7635e6a 100644\n>> --- a/t/unit-tests/unit-test.c\n>> +++ b/t/unit-tests/unit-test.c\n>> @@ -6,10 +6,9 @@ int cmd_main(int argc, const char **argv)\n>>   \tint ret;\n>>     \t/* Append the \"-t\" flag such that the tests generate TAP\n>> output. */\n>> -\tALLOC_ARRAY(argv_copy, argc + 2);\n>> +\tALLOC_ARRAY(argv_copy, argc + 1);\n>>   \tCOPY_ARRAY(argv_copy, argv, argc);\n>>   \targv_copy[argc++] = \"-t\";\n>> -\targv_copy[argc] = NULL;\n>\n> I'm confused by this - it looks like it belongs in the previous patch\n> but why are we deleting the line that adds the terminating NULL to\n> argv in the first place?\n\nI agree with you that it belongs to the previous step.  clar_test(),\nunlike the usual convention used by main(), works solely with argc\nto tell where the list of argument ends, without requiring argv[] to\nbe an NULL terminated array.  I do not know if clar _promises_ that\nas the calling convention to its users, but if it does so, and if we\nwant to take advantage of that promise and leave the terminating\nNULL out of argv[], we should do so from the beginning.\n\nA NULL-terminated argv[], as long as we make sure argc only counts\nthe real arguments without counting that terminating NULL, would not\nharm the clar_test(argc, argv) call, so I actually do not mind the\nway this caller was originally written, with terminating NULL.  It\nlooks a lot more familiar, even though we may be wasting memory at\nthe end of argv[] array.\n"},{"id":"501233","messageId":"xmqqo75qgws6.fsf@gitster.g","threadId":"61874","inReplyTo":"ca09d19fd51cd4b3072b339f483b6b6d6e467b56.1723791831.git.ps@pks.im","subject":"Re: [PATCH v5 9/9] t/unit-tests: convert ctype tests to use clar","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-18T06:39:05Z","receivedAt":"2024-08-18T06:39:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Convert the ctype tests to use the new clar unit testing framework.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Makefile                            |  2 +-\n>  t/unit-tests/{t-ctype.c => ctype.c} | 71 +++++++++++++++++++++++------\n>  2 files changed, 59 insertions(+), 14 deletions(-)\n>  rename t/unit-tests/{t-ctype.c => ctype.c} (71%)\n\nIn 'next' we use the if_test stuff to simplify the ctype unit tests.\nThis gives a good example to illustrate the strengths and weakness\nof these approaches.  I resolved the conflict in 'seen' with this\ntopic in favor of clar tests so that it becomes easier to compare.\n\n>  #define TEST_CHAR_CLASS(class, string) do { \\\n>  \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n>  \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n>  \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n> -\tint skip = test__run_begin(); \\\n> -\tif (!skip) { \\\n> -\t\tfor (int i = 0; i < 256; i++) { \\\n> -\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n> -\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n> -\t\t} \\\n> -\t\tcheck(!class(EOF)); \\\n> -\t} \\\n> -\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n> +\tfor (int i = 0; i < 256; i++) \\\n> +\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n> +\tcl_assert(!class(EOF)); \\\n>  } while (0)\n"},{"id":"501303","messageId":"xmqqr0akcion.fsf@gitster.g","threadId":"61874","inReplyTo":"369060793306daab1796ae3063b4eaacc287f12d.1723791831.git.ps@pks.im","subject":"Re: [PATCH v5 2/9] t: import the clar unit testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-19T21:21:44Z","receivedAt":"2024-08-19T21:21:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\n> new file mode 100644\n> index 0000000000..3fc2c76815\n> --- /dev/null\n> +++ b/t/unit-tests/clar/clar.c\n> @@ -0,0 +1,842 @@\n> ...\n> +\n> +#ifdef _WIN32\n> +#\tinclude <windows.h>\n> +#\tinclude <io.h>\n> +#\tinclude <shellapi.h>\n> +#\tinclude <direct.h>\n\nThis seems to break Windows build\n\n  In file included from t/unit-tests/clar/clar.c:22:\n  D:/git-sdk-64-minimal/mingw64/include/windows.h:89:10: fatal error: shellapi.h: No such file or directory\n     89 | #include <shellapi.h>\n        |          ^~~~~~~~~~~~\n  compilation terminated.\n\nhttps://github.com/git/git/actions/runs/10459342252/job/28963152995\n\nAs we have other topics that I do want to do well in 'seen' before\nmerging them to 'next', I'd temporarily drop this topic from 'seen'\nand push out the rest of the topics.\n"},{"id":"501304","messageId":"01ed01daf281$c5dba4a0$5192ede0$@nexbridge.com","threadId":"61874","inReplyTo":"xmqqr0akcion.fsf@gitster.g","subject":"RE: [PATCH v5 2/9] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-19T21:50:06Z","receivedAt":"2024-08-19T21:50:31Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Monday, August 19, 2024 5:22 PM, Junio C Hamano wrote:\n>Patrick Steinhardt <ps@pks.im> writes:\n>\n>> diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c new\n>> file mode 100644 index 0000000000..3fc2c76815\n>> --- /dev/null\n>> +++ b/t/unit-tests/clar/clar.c\n>> @@ -0,0 +1,842 @@\n>> ...\n>> +\n>> +#ifdef _WIN32\n>> +#\tinclude <windows.h>\n>> +#\tinclude <io.h>\n>> +#\tinclude <shellapi.h>\n>> +#\tinclude <direct.h>\n>\n>This seems to break Windows build\n>\n>  In file included from t/unit-tests/clar/clar.c:22:\n>  D:/git-sdk-64-minimal/mingw64/include/windows.h:89:10: fatal error: shellapi.h:\n>No such file or directory\n>     89 | #include <shellapi.h>\n>        |          ^~~~~~~~~~~~\n>  compilation terminated.\n>\n>https://github.com/git/git/actions/runs/10459342252/job/28963152995\n>\n>As we have other topics that I do want to do well in 'seen' before merging them to\n>'next', I'd temporarily drop this topic from 'seen'\n>and push out the rest of the topics.\n\nshellapi.h is not portable. This breaks the NonStop build also.\n\n"},{"id":"501305","messageId":"xmqqmsl8cga2.fsf@gitster.g","threadId":"61874","inReplyTo":"01ed01daf281$c5dba4a0$5192ede0$@nexbridge.com","subject":"Re: [PATCH v5 2/9] t: import the clar unit testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-19T22:13:41Z","receivedAt":"2024-08-19T22:13:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"<rsbecker@nexbridge.com> writes:\n\n> On Monday, August 19, 2024 5:22 PM, Junio C Hamano wrote:\n>>Patrick Steinhardt <ps@pks.im> writes:\n>>\n>>> diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c new\n>>> file mode 100644 index 0000000000..3fc2c76815\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/clar.c\n>>> @@ -0,0 +1,842 @@\n>>> ...\n>>> +\n>>> +#ifdef _WIN32\n>>> +#\tinclude <windows.h>\n>>> +#\tinclude <io.h>\n>>> +#\tinclude <shellapi.h>\n>>> +#\tinclude <direct.h>\n>>\n>>This seems to break Windows build\n>>\n>>  In file included from t/unit-tests/clar/clar.c:22:\n>>  D:/git-sdk-64-minimal/mingw64/include/windows.h:89:10: fatal error: shellapi.h:\n>>No such file or directory\n>>     89 | #include <shellapi.h>\n>>        |          ^~~~~~~~~~~~\n>>  compilation terminated.\n>>\n>>https://github.com/git/git/actions/runs/10459342252/job/28963152995\n>>\n>>As we have other topics that I do want to do well in 'seen' before merging them to\n>>'next', I'd temporarily drop this topic from 'seen'\n>>and push out the rest of the topics.\n>\n> shellapi.h is not portable. This breaks the NonStop build also.\n\nEven inside \"#ifdef _WIN32\"?\n"},{"id":"501306","messageId":"01ee01daf288$97977310$c6c65930$@nexbridge.com","threadId":"61874","inReplyTo":"xmqqmsl8cga2.fsf@gitster.g","subject":"RE: [PATCH v5 2/9] t: import the clar unit testing framework","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2024-08-19T22:38:56Z","receivedAt":"2024-08-19T22:39:12Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Monday, August 19, 2024 6:14 PM, Junio C Hamano wrote:\n><rsbecker@nexbridge.com> writes:\n>\n>> On Monday, August 19, 2024 5:22 PM, Junio C Hamano wrote:\n>>>Patrick Steinhardt <ps@pks.im> writes:\n>>>\n>>>> diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c new\n>>>> file mode 100644 index 0000000000..3fc2c76815\n>>>> --- /dev/null\n>>>> +++ b/t/unit-tests/clar/clar.c\n>>>> @@ -0,0 +1,842 @@\n>>>> ...\n>>>> +\n>>>> +#ifdef _WIN32\n>>>> +#\tinclude <windows.h>\n>>>> +#\tinclude <io.h>\n>>>> +#\tinclude <shellapi.h>\n>>>> +#\tinclude <direct.h>\n>>>\n>>>This seems to break Windows build\n>>>\n>>>  In file included from t/unit-tests/clar/clar.c:22:\n>>>  D:/git-sdk-64-minimal/mingw64/include/windows.h:89:10: fatal error:\n>shellapi.h:\n>>>No such file or directory\n>>>     89 | #include <shellapi.h>\n>>>        |          ^~~~~~~~~~~~\n>>>  compilation terminated.\n>>>\n>>>https://github.com/git/git/actions/runs/10459342252/job/28963152995\n>>>\n>>>As we have other topics that I do want to do well in 'seen' before\n>>>merging them to 'next', I'd temporarily drop this topic from 'seen'\n>>>and push out the rest of the topics.\n>>\n>> shellapi.h is not portable. This breaks the NonStop build also.\n>\n>Even inside \"#ifdef _WIN32\"?\n\nGood point, but I did not see that in the prior review. My apologies.\n\n"},{"id":"501335","messageId":"ZsSTDbEB2nIjxvcN@tanuki","threadId":"61874","inReplyTo":"xmqq8qwwmoqv.fsf@gitster.g","subject":"Re: [PATCH v5 8/9] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T12:59:00Z","receivedAt":"2024-08-20T12:59:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Aug 16, 2024 at 09:11:36AM -0700, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n> \n> > Hi Patrick\n> >\n> > On 16/08/2024 08:04, Patrick Steinhardt wrote:\n> >> diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\n> >> index 32a81299e9..82b7635e6a 100644\n> >> --- a/t/unit-tests/unit-test.c\n> >> +++ b/t/unit-tests/unit-test.c\n> >> @@ -6,10 +6,9 @@ int cmd_main(int argc, const char **argv)\n> >>   \tint ret;\n> >>     \t/* Append the \"-t\" flag such that the tests generate TAP\n> >> output. */\n> >> -\tALLOC_ARRAY(argv_copy, argc + 2);\n> >> +\tALLOC_ARRAY(argv_copy, argc + 1);\n> >>   \tCOPY_ARRAY(argv_copy, argv, argc);\n> >>   \targv_copy[argc++] = \"-t\";\n> >> -\targv_copy[argc] = NULL;\n> >\n> > I'm confused by this - it looks like it belongs in the previous patch\n> > but why are we deleting the line that adds the terminating NULL to\n> > argv in the first place?\n> \n> I agree with you that it belongs to the previous step.  clar_test(),\n> unlike the usual convention used by main(), works solely with argc\n> to tell where the list of argument ends, without requiring argv[] to\n> be an NULL terminated array.  I do not know if clar _promises_ that\n> as the calling convention to its users, but if it does so, and if we\n> want to take advantage of that promise and leave the terminating\n> NULL out of argv[], we should do so from the beginning.\n\nAgreed.\n\n> A NULL-terminated argv[], as long as we make sure argc only counts\n> the real arguments without counting that terminating NULL, would not\n> harm the clar_test(argc, argv) call, so I actually do not mind the\n> way this caller was originally written, with terminating NULL.  It\n> looks a lot more familiar, even though we may be wasting memory at\n> the end of argv[] array.\n\nOkay. It's not necessary, but I don't mind it much, either.\n\nPatrick\n"},{"id":"501336","messageId":"ZsSTLHleGNC2zjw5@tanuki","threadId":"61874","inReplyTo":"xmqqr0akcion.fsf@gitster.g","subject":"Re: [PATCH v5 2/9] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T12:59:24Z","receivedAt":"2024-08-20T12:59:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Aug 19, 2024 at 02:21:44PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\n> > new file mode 100644\n> > index 0000000000..3fc2c76815\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/clar.c\n> > @@ -0,0 +1,842 @@\n> > ...\n> > +\n> > +#ifdef _WIN32\n> > +#\tinclude <windows.h>\n> > +#\tinclude <io.h>\n> > +#\tinclude <shellapi.h>\n> > +#\tinclude <direct.h>\n> \n> This seems to break Windows build\n> \n>   In file included from t/unit-tests/clar/clar.c:22:\n>   D:/git-sdk-64-minimal/mingw64/include/windows.h:89:10: fatal error: shellapi.h: No such file or directory\n>      89 | #include <shellapi.h>\n>         |          ^~~~~~~~~~~~\n>   compilation terminated.\n> \n> https://github.com/git/git/actions/runs/10459342252/job/28963152995\n> \n> As we have other topics that I do want to do well in 'seen' before\n> merging them to 'next', I'd temporarily drop this topic from 'seen'\n> and push out the rest of the topics.\n\nMakes sense. I (or rather Dscho, thanks!) will have a deeper look at\nWindows and then also rebase the branch to fix conflicts with the\nupdated tests in \"master\".\n\nPatrick\n"},{"id":"501337","messageId":"ZsSTNV3EeTJQ3Ywn@tanuki","threadId":"61874","inReplyTo":"4aa6f557-1dfc-4aaa-b6db-e47013568ecc@gmail.com","subject":"Re: [PATCH v5 9/9] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T12:59:33Z","receivedAt":"2024-08-20T12:59:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Aug 16, 2024 at 02:38:30PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 16/08/2024 08:05, Patrick Steinhardt wrote:\n> >   #define TEST_CHAR_CLASS(class, string) do { \\\n> >   \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n> >   \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n> >   \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n> > -\tint skip = test__run_begin(); \\\n> > -\tif (!skip) { \\\n> > -\t\tfor (int i = 0; i < 256; i++) { \\\n> > -\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n> > -\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n> > -\t\t} \\\n> > -\t\tcheck(!class(EOF)); \\\n> > -\t} \\\n> > -\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n> > +\tfor (int i = 0; i < 256; i++) \\\n> > +\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n> \n> If this fails how are we supposed to know which character it was checking?\n> \n> Thanks\n\nI'll introduce a new function `cl_failf()` that allows us to print\ninformation like this.\n\nPatrick\n"},{"id":"501338","messageId":"ZsSTOczuGhT5zwoV@tanuki","threadId":"61874","inReplyTo":"b87700d2-0c9a-4d0c-9ee4-e6a91278d596@gmail.com","subject":"Re: [PATCH v5 0/9] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T12:59:37Z","receivedAt":"2024-08-20T12:59:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Aug 16, 2024 at 02:37:34PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 16/08/2024 08:04, Patrick Steinhardt wrote:\n> > Hi,\n> > \n> > this is the fifth version of my patch series that introduces the clar\n> > testing framework for our unit tests.\n> \n> Thanks for working on this, I'm broadly in favor of this change. I\n> like the way it keeps each test as a function and adds automatic test\n> registration with support for setup and teardown functions. I am keen\n> though to keep an emphasis on good diagnostic messages when tests\n> fail. Looking at the conversions in this series all of the test_msg()\n> lines that provide useful debugging context are removed. I'm not sure\n> using yaml to report errors rather than human readable messages is an\n> improvement either.\n> \n> I wonder if we want to either improve the assertions offered by clar\n> or write our own. I find the names of the cl_assert_equal_?()\n> functions are a bit cumbersome. The aim of the check_* names was to\n> try and be both concise and descriptive. Adding our own check_* macros\n> on top of clar would also make it easier to port our existing tests.\n> \n> Here are some thought having read through the assertion and error\n> reporting code:\n> \n>  - As I think you've pointed out elsewhere there are no equivalents\n>    for check_int(a, <|<=|>|>=, b) so we're forced to use cl_assert()\n>    and forego the better diagnostic messages that come from a\n>    dedicated comparison macro. We should fix this as a priority.\n\nAgreed, this one also feels rather limiting to me. Are you okay with me\ndoing this as a follow-up in case this series lands?\n\n>  - cl_assert_equal_i() casts its arguments to int whereas check_int()\n>    and check_uint() are careful to avoid truncation and keep the\n>    original signedness (if that's a word). I think that's unlikely to\n>    be a problem with our current test but could trip us up in the\n>    future.\n\nYeah. If it ever becomes a problem we can likely just introduce\nsomething like `cl_assert_equal_u()` to achieve the same for unsigned.\nBoth should probably end up casting to `intmax_t` and `uintmax_t`,\nrespectively.\n\n>  - cl_assert_equal_s() prints each argument as-is. This means\n>    that it passes NULL arguments through to snprintf() which is\n>    undefined according to the C standard. Compare this to check_str()\n>    that is NULL safe and is careful to escape control characters and\n>    add delimiters to the beginning and end of the string to make it\n>    obvious when a string contains leading or trailing whitespace.\n\nGood point indeed, and something I'm happy to fix upstream.\n\n>  - The cl_assert_equal_?() macros lack type safety for the arguments\n>    being compared as they are wrappers around a variadic function.\n>    That could be fixed by having each macros wrap a dedicated\n>    function that wraps clar__fail().\n\nSome of them do indeed, others generate issues. I don't think we have to\nhave dedicated functions, but could do something about this with\n`__attribute__((format (printf, ...)))`.\n\n>  - There is no equivalent of test_todo() to mark assertions that are\n>    expected to fail. We're not using that yet in our tests but our\n>    experience with the integration tests suggests that we are likely\n>    to want this in the future.\n\nHeh, funny that you mention this. I had this discussion some 6 years ago\nI think, where I also mentioned that this should exist as a feature. In\nany case, I agree.\n\n>  - To me the \"sandbox\" feature is mis-named as it does not provide any\n>    confinement. It is instead a useful mechanism for running a test in\n>    a temporary directory created from a template.\n\nI guess we'll either just have to not use it or ignore that it's named a\nbit awkwardly. Changing this in clar probably wouldn't work well because\nother projects may depend on it.\n\n>  - There are no checks for failing memory allocations - the return\n>    value of calloc() and strdup() are used without checking for NULL.\n\nI'll commit to fixing this upstream if this lands.\n\n>  - The use of longjmp is a bit of a double edged sword as it makes it\n>    easy to leak resources on test failures.\n\nI have to say that this is one of the best features of the clar to me.\nThe current test framework we use doesn't, which in theory requires you\nto always `return` whenever there was an error. But that results in code\nthat is both awful to read and write, so for most of the tests simply\ndon't bother at all. And consequently, the tests are quite likely to\ncause segfaults once one of the checks fails because we didn't abort\nrunning the testcase, but things are broken.\n\nIn practice, I'd claim that you don't typically care all that much about\nmemory leaks once your basic assertions start to fail.\n\nSo, things that need addressing and that I'm happy to do as follow ups:\n\n  - Introduce functions that compare integers.\n\n  - Improve type safety of the `cl_assert_equal_?()` macros.\n\n  - Adapt `cl_assert_equal_s()` to handle NULL pointers.\n\n  - Introduce checks for failing memory allocations.\n\nNice to have would be support for known-failing tests.\n\nPatrick\n"},{"id":"501339","messageId":"e48a64611377b2ad49c7a1144612f8c001006d30.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 01/13] t: do not pass GIT_TEST_OPTS to unit tests with prove","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:10Z","receivedAt":"2024-08-20T14:02:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When using the prove target, we append GIT_TEST_OPTS to the arguments\nthat we execute each of the tests with. This doesn't only include the\nintended test scripts, but also ends up passing the arguments to our\nunit tests. This is unintentional though as they do not even know to\ninterpret those arguments, and is inconsistent with how we execute unit\ntests without prove.\n\nThis isn't much of an issue because our current set of unit tests mostly\nignore their arguments anyway. With the introduction of clar-based unit\ntests this is about to become an issue though, as these do parse their\ncommand line argument to alter behaviour.\n\nPrepare for this by passing GIT_TEST_OPTS to \"run-test.sh\" via an\nenvironment variable. Like this, we can conditionally forward it to our\ntest scripts, only.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/Makefile    | 3 ++-\n t/run-test.sh | 2 +-\n 2 files changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/Makefile b/t/Makefile\nindex 4c30e7c06fb..d2212de0b78 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -68,7 +68,8 @@ failed:\n \ttest -z \"$$failed\" || $(MAKE) $$failed\n \n prove: pre-clean check-chainlint $(TEST_LINT)\n-\t@echo \"*** prove (shell & unit tests) ***\"; $(CHAINLINTSUPPRESS) TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS) :: $(GIT_TEST_OPTS)\n+\t@echo \"*** prove (shell & unit tests) ***\"\n+\t@$(CHAINLINTSUPPRESS) TEST_OPTIONS='$(GIT_TEST_OPTS)' TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS)\n \t$(MAKE) clean-except-prove-cache\n \n $(T):\ndiff --git a/t/run-test.sh b/t/run-test.sh\nindex 13c353b91b4..63328ac630c 100755\n--- a/t/run-test.sh\n+++ b/t/run-test.sh\n@@ -10,7 +10,7 @@ case \"$1\" in\n \t\techo >&2 \"ERROR: TEST_SHELL_PATH is empty or not set\"\n \t\texit 1\n \tfi\n-\texec \"${TEST_SHELL_PATH}\" \"$@\"\n+\texec \"${TEST_SHELL_PATH}\" \"$@\" ${TEST_OPTIONS}\n \t;;\n *)\n \texec \"$@\"\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501340","messageId":"cover.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[PATCH v6 00/13] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:07Z","receivedAt":"2024-08-20T14:02:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is another version of my patch series that introduces the clar\ntesting framework for our C unit tests.\n\nChanges compared to v5:\n\n  - Rebased on top of `master` to fix some merge conflicts. This series\n    is now built on bb9c16bd4f (The sixth batch, 2024-08-19).\n\n  - Introduce `cl_failf()` as a nice way to print formatted error\n    messages.\n\n  - NULL-terminate the argv array again and move the changes into the\n    correct patch.\n\n  - Most of the changes were done by Dscho to port clar over to Windows,\n    including CMake build instructions to make this usable with MSVC. I\n    plan to upstream these changes when this series lands in Git.\n\n  - Introduce CMake support, also by Dscho.\n\nThanks for all the feedback, and thanks to Dscho for helping out with\nporting this to Windows.\n\nGitHub pipeline: https://github.com/git/git/actions/runs/10472008177/\nGitLab pipeline: https://gitlab.com/gitlab-org/git/-/pipelines/1419946971\n\nPatrick\n\nJohannes Schindelin (4):\n  clar: avoid compile error with mingw-w64\n  clar(win32): avoid compile error due to unused `fs_copy()`\n  clar: stop including `shellapi.h` unnecessarily\n  clar: add CMake support\n\nPatrick Steinhardt (9):\n  t: do not pass GIT_TEST_OPTS to unit tests with prove\n  t: import the clar unit testing framework\n  t/clar: fix compatibility with NonStop\n  Makefile: fix sparse dependency on GENERATED_H\n  Makefile: make hdr-check depend on generated headers\n  Makefile: do not use sparse on third-party sources\n  Makefile: wire up the clar unit testing framework\n  t/unit-tests: convert strvec tests to use clar\n  t/unit-tests: convert ctype tests to use clar\n\n .gitignore                                 |   1 +\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |  53 +-\n contrib/buildsystems/CMakeLists.txt        |  53 ++\n t/Makefile                                 |   4 +-\n t/run-test.sh                              |   2 +-\n t/unit-tests/.gitignore                    |   2 +\n t/unit-tests/clar-generate.awk             |  50 ++\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 524 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n t/unit-tests/{t-ctype.c => ctype.c}        |  70 +-\n t/unit-tests/strvec.c                      | 222 ++++++\n t/unit-tests/t-strvec.c                    | 211 ------\n t/unit-tests/unit-test.c                   |  18 +\n t/unit-tests/unit-test.h                   |  10 +\n 31 files changed, 3409 insertions(+), 235 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n rename t/unit-tests/{t-ctype.c => ctype.c} (70%)\n create mode 100644 t/unit-tests/strvec.c\n delete mode 100644 t/unit-tests/t-strvec.c\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\nRange-diff against v5:\n 1:  832dc0496fb =  1:  e48a6461137 t: do not pass GIT_TEST_OPTS to unit tests with prove\n 2:  36906079330 =  2:  1710e9f9ff7 t: import the clar unit testing framework\n 3:  db53673294e =  3:  5c21aa87aa2 t/clar: fix compatibility with NonStop\n -:  ----------- >  4:  06d2bce0d82 clar: avoid compile error with mingw-w64\n -:  ----------- >  5:  f88b3421a09 clar(win32): avoid compile error due to unused `fs_copy()`\n -:  ----------- >  6:  5fb4c55be33 clar: stop including `shellapi.h` unnecessarily\n 4:  b6199c88dd7 =  7:  e0dcbd5ca83 Makefile: fix sparse dependency on GENERATED_H\n 5:  06364b2b722 =  8:  77a03f8df70 Makefile: make hdr-check depend on generated headers\n 6:  88ea94ce16c =  9:  c91dd7327e3 Makefile: do not use sparse on third-party sources\n 7:  05bcb5bef6c = 10:  115c15aa9ae Makefile: wire up the clar unit testing framework\n 8:  8f56b4d6264 ! 11:  b3b8df04872 t/unit-tests: convert strvec tests to use clar\n    @@ Makefile: THIRD_PARTY_SOURCES += sha1dc/%\n      UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n      UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n      UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n    -@@ Makefile: UNIT_TEST_PROGRAMS += t-reftable-merged\n    - UNIT_TEST_PROGRAMS += t-reftable-record\n    +@@ Makefile: UNIT_TEST_PROGRAMS += t-reftable-record\n    + UNIT_TEST_PROGRAMS += t-reftable-tree\n      UNIT_TEST_PROGRAMS += t-strbuf\n      UNIT_TEST_PROGRAMS += t-strcmp-offset\n     -UNIT_TEST_PROGRAMS += t-strvec\n    @@ Makefile: UNIT_TEST_PROGRAMS += t-reftable-merged\n      UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n      UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\n     \n    - ## t/unit-tests/t-strvec.c => t/unit-tests/strvec.c ##\n    + ## t/unit-tests/strvec.c (new) ##\n     @@\n    --#include \"test-lib.h\"\n     +#include \"unit-test.h\"\n    - #include \"strbuf.h\"\n    - #include \"strvec.h\"\n    - \n    - #define check_strvec(vec, ...) \\\n    - \tdo { \\\n    - \t\tconst char *expect[] = { __VA_ARGS__ }; \\\n    --\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n    --\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n    --\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n    --\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n    --\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n    --\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n    --\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n    --\t\t\t\t\t\t (uintmax_t)i); \\\n    --\t\t\t\t\tbreak; \\\n    --\t\t\t\t} \\\n    --\t\t\t} \\\n    --\t\t} \\\n    ++#include \"strbuf.h\"\n    ++#include \"strvec.h\"\n    ++\n    ++#define check_strvec(vec, ...) \\\n    ++\tdo { \\\n    ++\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n     +\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n     +\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n     +\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n     +\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n     +\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n     +\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n    - \t} while (0)\n    - \n    --static void t_static_init(void)\n    ++\t} while (0)\n    ++\n     +void test_strvec__init(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    --\tcheck_pointer_eq(vec.v, empty_strvec);\n    --\tcheck_uint(vec.nr, ==, 0);\n    --\tcheck_uint(vec.alloc, ==, 0);\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n     +\tcl_assert_equal_i(vec.nr, 0);\n     +\tcl_assert_equal_i(vec.alloc, 0);\n    - }\n    - \n    --static void t_dynamic_init(void)\n    ++}\n    ++\n     +void test_strvec__dynamic_init(void)\n    - {\n    - \tstruct strvec vec;\n    - \tstrvec_init(&vec);\n    --\tcheck_pointer_eq(vec.v, empty_strvec);\n    --\tcheck_uint(vec.nr, ==, 0);\n    --\tcheck_uint(vec.alloc, ==, 0);\n    ++{\n    ++\tstruct strvec vec;\n    ++\tstrvec_init(&vec);\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n     +\tcl_assert_equal_i(vec.nr, 0);\n     +\tcl_assert_equal_i(vec.alloc, 0);\n    - }\n    - \n    --static void t_clear(void)\n    ++}\n    ++\n     +void test_strvec__clear(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_push(&vec, \"foo\");\n    - \tstrvec_clear(&vec);\n    --\tcheck_pointer_eq(vec.v, empty_strvec);\n    --\tcheck_uint(vec.nr, ==, 0);\n    --\tcheck_uint(vec.alloc, ==, 0);\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_push(&vec, \"foo\");\n    ++\tstrvec_clear(&vec);\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n     +\tcl_assert_equal_i(vec.nr, 0);\n     +\tcl_assert_equal_i(vec.alloc, 0);\n    - }\n    - \n    --static void t_push(void)\n    ++}\n    ++\n     +void test_strvec__push(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \n    -@@ t/unit-tests/strvec.c: static void t_push(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_pushf(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\n    ++\tstrvec_push(&vec, \"foo\");\n    ++\tcheck_strvec(&vec, \"foo\", NULL);\n    ++\n    ++\tstrvec_push(&vec, \"bar\");\n    ++\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    ++\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__pushf(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushf(&vec, \"foo: %d\", 1);\n    -@@ t/unit-tests/strvec.c: static void t_pushf(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_pushl(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushf(&vec, \"foo: %d\", 1);\n    ++\tcheck_strvec(&vec, \"foo: 1\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__pushl(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_pushl(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_pushv(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__pushv(void)\n    - {\n    - \tconst char *strings[] = {\n    - \t\t\"foo\", \"bar\", \"baz\", NULL,\n    -@@ t/unit-tests/strvec.c: static void t_pushv(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_replace_at_head(void)\n    ++{\n    ++\tconst char *strings[] = {\n    ++\t\t\"foo\", \"bar\", \"baz\", NULL,\n    ++\t};\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\n    ++\tstrvec_pushv(&vec, strings);\n    ++\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__replace_at_head(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_replace_at_head(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_replace_at_tail(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_replace(&vec, 0, \"replaced\");\n    ++\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__replace_at_tail(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_replace_at_tail(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_replace_in_between(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_replace(&vec, 2, \"replaced\");\n    ++\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__replace_in_between(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_replace_in_between(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_replace_with_substring(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_replace(&vec, 1, \"replaced\");\n    ++\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__replace_with_substring(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_replace_with_substring(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_remove_at_head(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", NULL);\n    ++\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n    ++\tcheck_strvec(&vec, \"oo\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__remove_at_head(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_remove_at_head(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_remove_at_tail(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_remove(&vec, 0);\n    ++\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__remove_at_tail(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_remove_at_tail(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_remove_in_between(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_remove(&vec, 2);\n    ++\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__remove_in_between(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_remove_in_between(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_pop_empty_array(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_remove(&vec, 1);\n    ++\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__pop_empty_array(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pop(&vec);\n    -@@ t/unit-tests/strvec.c: static void t_pop_empty_array(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_pop_non_empty_array(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pop(&vec);\n    ++\tcheck_strvec(&vec, NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__pop_non_empty_array(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    -@@ t/unit-tests/strvec.c: static void t_pop_non_empty_array(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_split_empty_string(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_pop(&vec);\n    ++\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__split_empty_string(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_split(&vec, \"\");\n    -@@ t/unit-tests/strvec.c: static void t_split_empty_string(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_split_single_item(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_split(&vec, \"\");\n    ++\tcheck_strvec(&vec, NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__split_single_item(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_split(&vec, \"foo\");\n    -@@ t/unit-tests/strvec.c: static void t_split_single_item(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_split_multiple_items(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_split(&vec, \"foo\");\n    ++\tcheck_strvec(&vec, \"foo\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__split_multiple_items(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_split(&vec, \"foo bar baz\");\n    -@@ t/unit-tests/strvec.c: static void t_split_multiple_items(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_split_whitespace_only(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_split(&vec, \"foo bar baz\");\n    ++\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__split_whitespace_only(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_split(&vec, \" \\t\\n\");\n    -@@ t/unit-tests/strvec.c: static void t_split_whitespace_only(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_split_multiple_consecutive_whitespaces(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_split(&vec, \" \\t\\n\");\n    ++\tcheck_strvec(&vec, NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__split_multiple_consecutive_whitespaces(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tstrvec_split(&vec, \"foo\\n\\t bar\");\n    -@@ t/unit-tests/strvec.c: static void t_split_multiple_consecutive_whitespaces(void)\n    - \tstrvec_clear(&vec);\n    - }\n    - \n    --static void t_detach(void)\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tstrvec_split(&vec, \"foo\\n\\t bar\");\n    ++\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    ++\tstrvec_clear(&vec);\n    ++}\n    ++\n     +void test_strvec__detach(void)\n    - {\n    - \tstruct strvec vec = STRVEC_INIT;\n    - \tconst char **detached;\n    -@@ t/unit-tests/strvec.c: static void t_detach(void)\n    - \tstrvec_push(&vec, \"foo\");\n    - \n    - \tdetached = strvec_detach(&vec);\n    --\tcheck_str(detached[0], \"foo\");\n    --\tcheck_pointer_eq(detached[1], NULL);\n    ++{\n    ++\tstruct strvec vec = STRVEC_INIT;\n    ++\tconst char **detached;\n    ++\n    ++\tstrvec_push(&vec, \"foo\");\n    ++\n    ++\tdetached = strvec_detach(&vec);\n     +\tcl_assert_equal_s(detached[0], \"foo\");\n     +\tcl_assert_equal_p(detached[1], NULL);\n    - \n    --\tcheck_pointer_eq(vec.v, empty_strvec);\n    --\tcheck_uint(vec.nr, ==, 0);\n    --\tcheck_uint(vec.alloc, ==, 0);\n    ++\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n     +\tcl_assert_equal_i(vec.nr, 0);\n     +\tcl_assert_equal_i(vec.alloc, 0);\n    - \n    - \tfree((char *) detached[0]);\n    - \tfree(detached);\n    - }\n    ++\n    ++\tfree((char *) detached[0]);\n    ++\tfree(detached);\n    ++}\n    +\n    + ## t/unit-tests/t-strvec.c (deleted) ##\n    +@@\n    +-#include \"test-lib.h\"\n    +-#include \"strbuf.h\"\n    +-#include \"strvec.h\"\n    +-\n    +-#define check_strvec(vec, ...) \\\n    +-\tdo { \\\n    +-\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n    +-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n    +-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n    +-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n    +-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n    +-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n    +-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n    +-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n    +-\t\t\t\t\t\t (uintmax_t)i); \\\n    +-\t\t\t\t\tbreak; \\\n    +-\t\t\t\t} \\\n    +-\t\t\t} \\\n    +-\t\t} \\\n    +-\t} while (0)\n     -\n     -int cmd_main(int argc, const char **argv)\n     -{\n    --\tTEST(t_static_init(), \"static initialization\");\n    --\tTEST(t_dynamic_init(), \"dynamic initialization\");\n    --\tTEST(t_clear(), \"clear\");\n    --\tTEST(t_push(), \"push\");\n    --\tTEST(t_pushf(), \"pushf\");\n    --\tTEST(t_pushl(), \"pushl\");\n    --\tTEST(t_pushv(), \"pushv\");\n    --\tTEST(t_replace_at_head(), \"replace at head\");\n    --\tTEST(t_replace_in_between(), \"replace in between\");\n    --\tTEST(t_replace_at_tail(), \"replace at tail\");\n    --\tTEST(t_replace_with_substring(), \"replace with substring\");\n    --\tTEST(t_remove_at_head(), \"remove at head\");\n    --\tTEST(t_remove_in_between(), \"remove in between\");\n    --\tTEST(t_remove_at_tail(), \"remove at tail\");\n    --\tTEST(t_pop_empty_array(), \"pop with empty array\");\n    --\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n    --\tTEST(t_split_empty_string(), \"split empty string\");\n    --\tTEST(t_split_single_item(), \"split single item\");\n    --\tTEST(t_split_multiple_items(), \"split multiple items\");\n    --\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n    --\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n    --\tTEST(t_detach(), \"detach\");\n    +-\tif_test (\"static initialization\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n    +-\t\tcheck_uint(vec.nr, ==, 0);\n    +-\t\tcheck_uint(vec.alloc, ==, 0);\n    +-\t}\n    +-\n    +-\tif_test (\"dynamic initialization\") {\n    +-\t\tstruct strvec vec;\n    +-\t\tstrvec_init(&vec);\n    +-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n    +-\t\tcheck_uint(vec.nr, ==, 0);\n    +-\t\tcheck_uint(vec.alloc, ==, 0);\n    +-\t}\n    +-\n    +-\tif_test (\"clear\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_push(&vec, \"foo\");\n    +-\t\tstrvec_clear(&vec);\n    +-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n    +-\t\tcheck_uint(vec.nr, ==, 0);\n    +-\t\tcheck_uint(vec.alloc, ==, 0);\n    +-\t}\n    +-\n    +-\tif_test (\"push\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\n    +-\t\tstrvec_push(&vec, \"foo\");\n    +-\t\tcheck_strvec(&vec, \"foo\", NULL);\n    +-\n    +-\t\tstrvec_push(&vec, \"bar\");\n    +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    +-\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"pushf\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushf(&vec, \"foo: %d\", 1);\n    +-\t\tcheck_strvec(&vec, \"foo: 1\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"pushl\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"pushv\") {\n    +-\t\tconst char *strings[] = {\n    +-\t\t\t\"foo\", \"bar\", \"baz\", NULL,\n    +-\t\t};\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\n    +-\t\tstrvec_pushv(&vec, strings);\n    +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"replace at head\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_replace(&vec, 0, \"replaced\");\n    +-\t\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"replace at tail\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_replace(&vec, 2, \"replaced\");\n    +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"replace in between\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_replace(&vec, 1, \"replaced\");\n    +-\t\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"replace with substring\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", NULL);\n    +-\t\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n    +-\t\tcheck_strvec(&vec, \"oo\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"remove at head\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_remove(&vec, 0);\n    +-\t\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"remove at tail\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_remove(&vec, 2);\n    +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"remove in between\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_remove(&vec, 1);\n    +-\t\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"pop with empty array\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pop(&vec);\n    +-\t\tcheck_strvec(&vec, NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"pop with non-empty array\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_pop(&vec);\n    +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"split empty string\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_split(&vec, \"\");\n    +-\t\tcheck_strvec(&vec, NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"split single item\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_split(&vec, \"foo\");\n    +-\t\tcheck_strvec(&vec, \"foo\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"split multiple items\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_split(&vec, \"foo bar baz\");\n    +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"split whitespace only\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_split(&vec, \" \\t\\n\");\n    +-\t\tcheck_strvec(&vec, NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"split multiple consecutive whitespaces\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tstrvec_split(&vec, \"foo\\n\\t bar\");\n    +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    +-\t\tstrvec_clear(&vec);\n    +-\t}\n    +-\n    +-\tif_test (\"detach\") {\n    +-\t\tstruct strvec vec = STRVEC_INIT;\n    +-\t\tconst char **detached;\n    +-\n    +-\t\tstrvec_push(&vec, \"foo\");\n    +-\n    +-\t\tdetached = strvec_detach(&vec);\n    +-\t\tcheck_str(detached[0], \"foo\");\n    +-\t\tcheck_pointer_eq(detached[1], NULL);\n    +-\n    +-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n    +-\t\tcheck_uint(vec.nr, ==, 0);\n    +-\t\tcheck_uint(vec.alloc, ==, 0);\n    +-\n    +-\t\tfree((char *) detached[0]);\n    +-\t\tfree(detached);\n    +-\t}\n    +-\n     -\treturn test_done();\n     -}\n    -\n    - ## t/unit-tests/unit-test.c ##\n    -@@ t/unit-tests/unit-test.c: int cmd_main(int argc, const char **argv)\n    - \tint ret;\n    - \n    - \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n    --\tALLOC_ARRAY(argv_copy, argc + 2);\n    -+\tALLOC_ARRAY(argv_copy, argc + 1);\n    - \tCOPY_ARRAY(argv_copy, argv, argc);\n    - \targv_copy[argc++] = \"-t\";\n    --\targv_copy[argc] = NULL;\n    - \n    - \tret = clar_test(argc, (char **) argv_copy);\n    - \n 9:  ca09d19fd51 ! 12:  1ac2e48a7f2 t/unit-tests: convert ctype tests to use clar\n    @@ Commit message\n         t/unit-tests: convert ctype tests to use clar\n     \n         Convert the ctype tests to use the new clar unit testing framework.\n    +    Introduce a new function `cl_failf()` that allows us to print a\n    +    formatted error message, which we can use to point out which of the\n    +    characters was classified incorrectly. This results in output like this\n    +    on failure:\n    +\n    +        # start of suite 1: ctype\n    +        ok 1 - ctype::isspace\n    +        not ok 2 - ctype::isdigit\n    +            ---\n    +            reason: |\n    +              Test failed.\n    +              0x61 is classified incorrectly\n    +            at:\n    +              file: 't/unit-tests/ctype.c'\n    +              line: 38\n    +              function: 'test_ctype__isdigit'\n    +            ---\n    +        ok 3 - ctype::isalpha\n    +        ok 4 - ctype::isalnum\n    +        ok 5 - ctype::is_glob_special\n    +        ok 6 - ctype::is_regex_special\n    +        ok 7 - ctype::is_pathspec_magic\n    +        ok 8 - ctype::isascii\n    +        ok 9 - ctype::islower\n    +        ok 10 - ctype::isupper\n    +        ok 11 - ctype::iscntrl\n    +        ok 12 - ctype::ispunct\n    +        ok 13 - ctype::isxdigit\n    +        ok 14 - ctype::isprint\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ Makefile: THIRD_PARTY_SOURCES += sha1dc/%\n     -UNIT_TEST_PROGRAMS += t-ctype\n      UNIT_TEST_PROGRAMS += t-example-decorate\n      UNIT_TEST_PROGRAMS += t-hash\n    - UNIT_TEST_PROGRAMS += t-mem-pool\n    + UNIT_TEST_PROGRAMS += t-hashmap\n     \n      ## t/unit-tests/t-ctype.c => t/unit-tests/ctype.c ##\n     @@\n    @@ t/unit-tests/t-ctype.c => t/unit-tests/ctype.c\n      \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n      \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n      \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n    --\tint skip = test__run_begin(); \\\n    --\tif (!skip) { \\\n    +-\tif_test (#class \" works\") { \\\n     -\t\tfor (int i = 0; i < 256; i++) { \\\n     -\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n     -\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n     -\t\t} \\\n     -\t\tcheck(!class(EOF)); \\\n     -\t} \\\n    --\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n     +\tfor (int i = 0; i < 256; i++) \\\n    -+\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n    ++\t\tif (class(i) != !!memchr(string, i, len)) \\\n    ++\t\t\tcl_failf(\"0x%02x is classified incorrectly\", i); \\\n     +\tcl_assert(!class(EOF)); \\\n      } while (0)\n      \n    @@ t/unit-tests/t-ctype.c => t/unit-tests/ctype.c\n     +{\n     +\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n      }\n    +\n    + ## t/unit-tests/unit-test.h ##\n    +@@\n    + #include \"git-compat-util.h\"\n    + #include \"clar/clar.h\"\n    + #include \"clar-decls.h\"\n    ++#include \"strbuf.h\"\n    ++\n    ++#define cl_failf(fmt, ...) do { \\\n    ++\tchar *desc = xstrfmt(fmt, __VA_ARGS__); \\\n    ++\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n    ++\tfree(desc); \\\n    ++} while (0)\n -:  ----------- > 13:  131036c398e clar: add CMake support\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501342","messageId":"1710e9f9ff75cee76be6d5c1311344c6564e3801.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 02/13] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:13Z","receivedAt":"2024-08-20T14:02:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Import the clar unit testing framework at commit 1516124 (Merge pull\nrequest #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |   4 +-\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 153 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n 20 files changed, 2943 insertions(+), 1 deletion(-)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n\ndiff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt\nindex 206037ffb19..5a432b7b29c 100644\n--- a/Documentation/technical/unit-tests.txt\n+++ b/Documentation/technical/unit-tests.txt\n@@ -203,6 +203,7 @@ GitHub / GitLab stars to estimate this.\n :criterion: https://github.com/Snaipe/Criterion[Criterion]\n :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n :check: https://libcheck.github.io/check/[Check]\n+:clar: https://github.com/clar-test/clar[Clar]\n \n [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n |=====\n@@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n+{clar},{isc},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n |=====\n \n === Additional framework candidates\ndiff --git a/Makefile b/Makefile\nindex a87e18b317d..28742a60964 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1331,6 +1331,8 @@ THIRD_PARTY_SOURCES += compat/poll/%\n THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n@@ -3261,7 +3263,7 @@ $(SP_OBJ): %.sp: %.c %.o\n .PHONY: sparse\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n ifndef OPENSSL_SHA1\n \tEXCEPT_HDRS += sha1/openssl.h\n endif\ndiff --git a/t/unit-tests/clar/.github/workflows/ci.yml b/t/unit-tests/clar/.github/workflows/ci.yml\nnew file mode 100644\nindex 00000000000..b1ac2de460a\n--- /dev/null\n+++ b/t/unit-tests/clar/.github/workflows/ci.yml\n@@ -0,0 +1,23 @@\n+name: CI\n+\n+on:\n+  push:\n+    branches: [ main ]\n+  pull_request:\n+    branches: [ main ]\n+\n+jobs:\n+  build:\n+    strategy:\n+      matrix:\n+        os: [ ubuntu-latest, macos-latest ]\n+\n+    runs-on: ${{ matrix.os }}\n+\n+    steps:\n+    - name: Check out\n+      uses: actions/checkout@v2\n+    - name: Build\n+      run: |\n+        cd test\n+        make\ndiff --git a/t/unit-tests/clar/COPYING b/t/unit-tests/clar/COPYING\nnew file mode 100644\nindex 00000000000..8983817f0c9\n--- /dev/null\n+++ b/t/unit-tests/clar/COPYING\n@@ -0,0 +1,15 @@\n+ISC License\n+\n+Copyright (c) 2011-2015 Vicent Marti\n+\n+Permission to use, copy, modify, and/or distribute this software for any\n+purpose with or without fee is hereby granted, provided that the above\n+copyright notice and this permission notice appear in all copies.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ndiff --git a/t/unit-tests/clar/README.md b/t/unit-tests/clar/README.md\nnew file mode 100644\nindex 00000000000..a8961c5f10f\n--- /dev/null\n+++ b/t/unit-tests/clar/README.md\n@@ -0,0 +1,329 @@\n+Come out and Clar\n+=================\n+\n+In Catalan, \"clar\" means clear, easy to perceive.  Using clar will make it\n+easy to test and make clear the quality of your code.\n+\n+> _Historical note_\n+>\n+> Originally the clar project was named \"clay\" because the word \"test\" has its\n+> roots in the latin word *\"testum\"*, meaning \"earthen pot\", and *\"testa\"*,\n+> meaning \"piece of burned clay\"?\n+>\n+> This is because historically, testing implied melting metal in a pot to\n+> check its quality.  Clay is what tests are made of.\n+\n+## Quick Usage Overview\n+\n+Clar is a minimal C unit testing framework. It's been written to replace the\n+old framework in [libgit2][libgit2], but it's both very versatile and\n+straightforward to use.\n+\n+Can you count to funk?\n+\n+- **Zero: Initialize test directory**\n+\n+    ~~~~ sh\n+    $ mkdir tests\n+    $ cp -r $CLAR_ROOT/clar* tests\n+    $ cp $CLAR_ROOT/test/clar_test.h tests\n+    $ cp $CLAR_ROOT/test/main.c.sample tests/main.c\n+    ~~~~\n+\n+- **One: Write some tests**\n+\n+    File: tests/adding.c:\n+\n+    ~~~~ c\n+    /* adding.c for the \"Adding\" suite */\n+    #include \"clar.h\"\n+\n+    static int *answer;\n+\n+    void test_adding__initialize(void)\n+    {\n+        answer = malloc(sizeof(int));\n+        cl_assert_(answer != NULL, \"No memory left?\");\n+        *answer = 42;\n+    }\n+\n+    void test_adding__cleanup(void)\n+    {\n+        free(answer);\n+    }\n+\n+    void test_adding__make_sure_math_still_works(void)\n+    {\n+        cl_assert_(5 > 3, \"Five should probably be greater than three\");\n+        cl_assert_(-5 < 2, \"Negative numbers are small, I think\");\n+        cl_assert_(*answer == 42, \"The universe is doing OK. And the initializer too.\");\n+    }\n+    ~~~~~\n+\n+- **Two: Build the test executable**\n+\n+    ~~~~ sh\n+    $ cd tests\n+    $ $CLAR_PATH/generate.py .\n+    Written `clar.suite` (1 suites)\n+    $ gcc -I. clar.c main.c adding.c -o testit\n+    ~~~~\n+\n+- **Funk: Funk it.**\n+\n+    ~~~~ sh\n+    $ ./testit\n+    ~~~~\n+\n+## The Clar Test Suite\n+\n+Writing a test suite is pretty straightforward. Each test suite is a `*.c`\n+file with a descriptive name: this encourages modularity.\n+\n+Each test suite has optional initialize and cleanup methods. These methods\n+will be called before and after running **each** test in the suite, even if\n+such test fails. As a rule of thumb, if a test needs a different initializer\n+or cleanup method than another test in the same module, that means it\n+doesn't belong in that module. Keep that in mind when grouping tests\n+together.\n+\n+The `initialize` and `cleanup` methods have the following syntax, with\n+`suitename` being the current suite name, e.g. `adding` for the `adding.c`\n+suite.\n+\n+~~~~ c\n+void test_suitename__initialize(void)\n+{\n+    /* init */\n+}\n+\n+void test_suitename__cleanup(void)\n+{\n+    /* cleanup */\n+}\n+~~~~\n+\n+These methods are encouraged to use static, global variables to store the state\n+that will be used by all tests inside the suite.\n+\n+~~~~ c\n+static git_repository *_repository;\n+\n+void test_status__initialize(void)\n+{\n+    create_tmp_repo(STATUS_REPO);\n+    git_repository_open(_repository, STATUS_REPO);\n+}\n+\n+void test_status__cleanup(void)\n+{\n+    git_repository_close(_repository);\n+    git_path_rm(STATUS_REPO);\n+}\n+\n+void test_status__simple_test(void)\n+{\n+    /* do something with _repository */\n+}\n+~~~~\n+\n+Writing the actual tests is just as straightforward. Tests have the\n+`void test_suitename__test_name(void)` signature, and they should **not**\n+be static. Clar will automatically detect and list them.\n+\n+Tests are run as they appear on their original suites: they have no return\n+value. A test is considered \"passed\" if it doesn't raise any errors. Check\n+the \"Clar API\" section to see the various helper functions to check and\n+raise errors during test execution.\n+\n+__Caution:__ If you use assertions inside of `test_suitename__initialize`,\n+make sure that you do not rely on `__initialize` being completely run\n+inside your `test_suitename__cleanup` function. Otherwise you might\n+encounter ressource cleanup twice.\n+\n+## How does Clar work?\n+\n+To use Clar:\n+\n+1. copy the Clar boilerplate to your test directory\n+2. copy (and probably modify) the sample `main.c` (from\n+   `$CLAR_PATH/test/main.c.sample`)\n+3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and\n+   write out the test suite metadata.\n+4. compile your test files and the Clar boilerplate into a single test\n+   executable\n+5. run the executable to test!\n+\n+The Clar boilerplate gives you a set of useful test assertions and features\n+(like accessing or making sandbox copies of fixture data).  It consists of\n+the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory.\n+You should not need to edit these files.\n+\n+The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes\n+`clar_test(argc, argv)` to run the tests.  Usually, you will edit this file\n+to perform any framework specific initialization and teardown that you need.\n+\n+The Clar mixer (`generate.py`) recursively scans your test directory for\n+any `.c` files, parses them, and writes the `clar.suite` file with all of\n+the metadata about your tests.  When you build, the `clar.suite` file is\n+included into `clar.c`.\n+\n+The mixer can be run with **Python 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and PyPy 1.6**.\n+\n+Commandline usage of the mixer is as follows:\n+\n+    $ ./generate.py .\n+\n+Where `.` is the folder where all the test suites can be found. The mixer\n+will automatically locate all the relevant source files and build the\n+testing metadata. The metadata will be written to `clar.suite`, in the same\n+folder as all the test suites. This file is included by `clar.c` and so\n+must be accessible via `#include` when building the test executable.\n+\n+    $ gcc -I. clar.c main.c suite1.c test2.c -o run_tests\n+\n+**Note that the Clar mixer only needs to be ran when adding new tests to a\n+suite, in order to regenerate the metadata**. As a result, the `clar.suite`\n+file can be checked into version control if you wish to be able to build\n+your test suite without having to re-run the mixer.\n+\n+This is handy when e.g. generating tests in a local computer, and then\n+building and testing them on an embedded device or a platform where Python\n+is not available.\n+\n+### Fixtures\n+\n+Clar can create sandboxed fixtures for you to use in your test. You'll need to compile *clar.c* with an additional `CFLAG`, `-DCLAR_FIXTURE_PATH`. This should be an absolute path to your fixtures directory.\n+\n+Once that's done, you can use the fixture API as defined below.\n+\n+## The Clar API\n+\n+Clar makes the following methods available from all functions in a test\n+suite.\n+\n+-   `cl_must_pass(call)`, `cl_must_pass_(call, message)`: Verify that the given\n+    function call passes, in the POSIX sense (returns a value greater or equal\n+    to 0).\n+\n+-   `cl_must_fail(call)`, `cl_must_fail_(call, message)`: Verify that the given\n+    function call fails, in the POSIX sense (returns a value less than 0).\n+\n+-   `cl_assert(expr)`, `cl_assert_(expr, message)`: Verify that `expr` is true.\n+\n+-   `cl_check_pass(call)`, `cl_check_pass_(call, message)`: Verify that the\n+    given function call passes, in the POSIX sense (returns a value greater or\n+    equal to 0). If the function call doesn't succeed, a test failure will be\n+    logged but the test's execution will continue.\n+\n+-   `cl_check_fail(call)`, `cl_check_fail_(call, message)`: Verify that the\n+    given function call fails, in the POSIX sense (returns a value less than\n+    0). If the function call doesn't fail, a test failure will be logged but\n+    the test's execution will continue.\n+\n+-   `cl_check(expr)`: Verify that `expr` is true. If `expr` is not\n+    true, a test failure will be logged but the test's execution will continue.\n+\n+-   `cl_fail(message)`: Fail the current test with the given message.\n+\n+-   `cl_warning(message)`: Issue a warning. This warning will be\n+    logged as a test failure but the test's execution will continue.\n+\n+-   `cl_set_cleanup(void (*cleanup)(void *), void *opaque)`: Set the cleanup\n+    method for a single test. This method will be called with `opaque` as its\n+    argument before the test returns (even if the test has failed).\n+    If a global cleanup method is also available, the local cleanup will be\n+    called first, and then the global.\n+\n+-   `cl_assert_equal_i(int,int)`: Verify that two integer values are equal.\n+    The advantage of this over a simple `cl_assert` is that it will format\n+    a much nicer error report if the values are not equal.\n+\n+-   `cl_assert_equal_s(const char *,const char *)`: Verify that two strings\n+    are equal.  The expected value can also be NULL and this will correctly\n+    test for that.\n+\n+-   `cl_fixture_sandbox(const char *)`: Sets up a sandbox for a fixture\n+    so that you can mutate the file directly.\n+\n+-   `cl_fixture_cleanup(const char *)`: Tears down the previous fixture\n+    sandbox.\n+\n+-   `cl_fixture(const char *)`: Gets the full path to a fixture file.\n+\n+Please do note that these methods are *always* available whilst running a\n+test, even when calling auxiliary/static functions inside the same file.\n+\n+It's strongly encouraged to perform test assertions in auxiliary methods,\n+instead of returning error values. This is considered good Clar style.\n+\n+Style Example:\n+\n+~~~~ c\n+/*\n+ * Bad style: auxiliary functions return an error code\n+ */\n+\n+static int check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    if (aux == NULL)\n+        return -1;\n+\n+    return strcmp(my_function(aux), str) == 0 ? 0 : -1;\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    cl_must_pass_(\n+        check_string(\"foo\"),\n+        \"String differs after processing\"\n+    );\n+\n+    cl_must_pass_(\n+        check_string(\"bar\"),\n+        \"String differs after processing\"\n+    );\n+}\n+~~~~\n+\n+~~~~ c\n+/*\n+ * Good style: auxiliary functions perform assertions\n+ */\n+\n+static void check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    cl_assert_(\n+        aux != NULL,\n+        \"String processing failed\"\n+    );\n+\n+    cl_assert_(\n+        strcmp(my_function(aux), str) == 0,\n+        \"String differs after processing\"\n+    );\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    check_string(\"foo\");\n+    check_string(\"bar\");\n+}\n+~~~~\n+\n+About Clar\n+==========\n+\n+Clar has been written from scratch by [Vicent Martí](https://github.com/vmg),\n+to replace the old testing framework in [libgit2][libgit2].\n+\n+Do you know what languages are *in* on the SF startup scene? Node.js *and*\n+Latin.  Follow [@vmg](https://www.twitter.com/vmg) on Twitter to\n+receive more lessons on word etymology. You can be hip too.\n+\n+\n+[libgit2]: https://github.com/libgit2/libgit2\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nnew file mode 100644\nindex 00000000000..3fc2c768158\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.c\n@@ -0,0 +1,842 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#include <assert.h>\n+#include <setjmp.h>\n+#include <stdlib.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <math.h>\n+#include <stdarg.h>\n+#include <wchar.h>\n+#include <time.h>\n+\n+/* required for sandboxing */\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#ifdef _WIN32\n+#\tinclude <windows.h>\n+#\tinclude <io.h>\n+#\tinclude <shellapi.h>\n+#\tinclude <direct.h>\n+\n+#\tdefine _MAIN_CC __cdecl\n+\n+#\tifndef stat\n+#\t\tdefine stat(path, st) _stat(path, st)\n+#\tendif\n+#\tifndef mkdir\n+#\t\tdefine mkdir(path, mode) _mkdir(path)\n+#\tendif\n+#\tifndef chdir\n+#\t\tdefine chdir(path) _chdir(path)\n+#\tendif\n+#\tifndef access\n+#\t\tdefine access(path, mode) _access(path, mode)\n+#\tendif\n+#\tifndef strdup\n+#\t\tdefine strdup(str) _strdup(str)\n+#\tendif\n+#\tifndef strcasecmp\n+#\t\tdefine strcasecmp(a,b) _stricmp(a,b)\n+#\tendif\n+\n+#\tifndef __MINGW32__\n+#\t\tpragma comment(lib, \"shell32\")\n+#\t\tifndef strncpy\n+#\t\t\tdefine strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)\n+#\t\tendif\n+#\t\tifndef W_OK\n+#\t\t\tdefine W_OK 02\n+#\t\tendif\n+#\t\tifndef S_ISDIR\n+#\t\t\tdefine S_ISDIR(x) ((x & _S_IFDIR) != 0)\n+#\t\tendif\n+#\t\tdefine p_snprintf(buf,sz,fmt,...) _snprintf_s(buf,sz,_TRUNCATE,fmt,__VA_ARGS__)\n+#\telse\n+#\t\tdefine p_snprintf snprintf\n+#\tendif\n+\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"Iu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"Ix\"\n+#\tendif\n+\n+#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+\ttypedef struct stat STAT_T;\n+#\telse\n+\ttypedef struct _stat STAT_T;\n+#\tendif\n+#else\n+#\tinclude <sys/wait.h> /* waitpid(2) */\n+#\tinclude <unistd.h>\n+#\tdefine _MAIN_CC\n+#\tdefine p_snprintf snprintf\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"zu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"zx\"\n+#\tendif\n+\ttypedef struct stat STAT_T;\n+#endif\n+\n+#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n+\n+#include \"clar.h\"\n+\n+static void fs_rm(const char *_source);\n+static void fs_copy(const char *_source, const char *dest);\n+\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name);\n+#endif\n+\n+struct clar_error {\n+\tconst char *file;\n+\tconst char *function;\n+\tsize_t line_number;\n+\tconst char *error_msg;\n+\tchar *description;\n+\n+\tstruct clar_error *next;\n+};\n+\n+struct clar_explicit {\n+\tsize_t suite_idx;\n+\tconst char *filter;\n+\n+\tstruct clar_explicit *next;\n+};\n+\n+struct clar_report {\n+\tconst char *test;\n+\tint test_number;\n+\tconst char *suite;\n+\n+\tenum cl_test_status status;\n+\ttime_t start;\n+\tdouble elapsed;\n+\n+\tstruct clar_error *errors;\n+\tstruct clar_error *last_error;\n+\n+\tstruct clar_report *next;\n+};\n+\n+struct clar_summary {\n+\tconst char *filename;\n+\tFILE *fp;\n+};\n+\n+static struct {\n+\tenum cl_test_status test_status;\n+\n+\tconst char *active_test;\n+\tconst char *active_suite;\n+\n+\tint total_skipped;\n+\tint total_errors;\n+\n+\tint tests_ran;\n+\tint suites_ran;\n+\n+\tenum cl_output_format output_format;\n+\n+\tint report_errors_only;\n+\tint exit_on_error;\n+\tint verbosity;\n+\n+\tint write_summary;\n+\tchar *summary_filename;\n+\tstruct clar_summary *summary;\n+\n+\tstruct clar_explicit *explicit;\n+\tstruct clar_explicit *last_explicit;\n+\n+\tstruct clar_report *reports;\n+\tstruct clar_report *last_report;\n+\n+\tvoid (*local_cleanup)(void *);\n+\tvoid *local_cleanup_payload;\n+\n+\tjmp_buf trampoline;\n+\tint trampoline_enabled;\n+\n+\tcl_trace_cb *pfn_trace_cb;\n+\tvoid *trace_payload;\n+\n+} _clar;\n+\n+struct clar_func {\n+\tconst char *name;\n+\tvoid (*ptr)(void);\n+};\n+\n+struct clar_suite {\n+\tconst char *name;\n+\tstruct clar_func initialize;\n+\tstruct clar_func cleanup;\n+\tconst struct clar_func *tests;\n+\tsize_t test_count;\n+\tint enabled;\n+};\n+\n+/* From clar_print_*.c */\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n+static void clar_print_onsuite(const char *suite_name, int suite_index);\n+static void clar_print_onabort(const char *msg, ...);\n+\n+/* From clar_sandbox.c */\n+static void clar_unsandbox(void);\n+static int clar_sandbox(void);\n+\n+/* From summary.h */\n+static struct clar_summary *clar_summary_init(const char *filename);\n+static int clar_summary_shutdown(struct clar_summary *fp);\n+\n+/* Load the declarations for the test suite */\n+#include \"clar.suite\"\n+\n+\n+#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n+\t} while (0)\n+\n+void cl_trace_register(cl_trace_cb *cb, void *payload)\n+{\n+\t_clar.pfn_trace_cb = cb;\n+\t_clar.trace_payload = payload;\n+}\n+\n+\n+/* Core test functions */\n+static void\n+clar_report_errors(struct clar_report *report)\n+{\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (error = report->errors; error; error = error->next)\n+\t\tclar_print_error(i++, _clar.last_report, error);\n+}\n+\n+static void\n+clar_report_all(void)\n+{\n+\tstruct clar_report *report;\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (report = _clar.reports; report; report = report->next) {\n+\t\tif (report->status != CL_TEST_FAILURE)\n+\t\t\tcontinue;\n+\n+\t\tfor (error = report->errors; error; error = error->next)\n+\t\t\tclar_print_error(i++, report, error);\n+\t}\n+}\n+\n+#ifdef WIN32\n+# define clar_time DWORD\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\t*out = GetTickCount();\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)*end - (double)*start) / 1000;\n+}\n+#else\n+# include <sys/time.h>\n+\n+# define clar_time struct timeval\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\tstruct timezone tz;\n+\n+\tgettimeofday(out, &tz);\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n+\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n+}\n+#endif\n+\n+static void\n+clar_run_test(\n+\tconst struct clar_suite *suite,\n+\tconst struct clar_func *test,\n+\tconst struct clar_func *initialize,\n+\tconst struct clar_func *cleanup)\n+{\n+\tclar_time start, end;\n+\n+\t_clar.trampoline_enabled = 1;\n+\n+\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n+\n+\t_clar.last_report->start = time(NULL);\n+\tclar_time_now(&start);\n+\n+\tif (setjmp(_clar.trampoline) == 0) {\n+\t\tif (initialize->ptr != NULL)\n+\t\t\tinitialize->ptr();\n+\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n+\t\ttest->ptr();\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n+\t}\n+\n+\tclar_time_now(&end);\n+\n+\t_clar.trampoline_enabled = 0;\n+\n+\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n+\t\t_clar.last_report->status = CL_TEST_OK;\n+\n+\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n+\n+\tif (_clar.local_cleanup != NULL)\n+\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n+\n+\tif (cleanup->ptr != NULL)\n+\t\tcleanup->ptr();\n+\n+\tCL_TRACE(CL_TRACE__TEST__END);\n+\n+\t_clar.tests_ran++;\n+\n+\t/* remove any local-set cleanup methods */\n+\t_clar.local_cleanup = NULL;\n+\t_clar.local_cleanup_payload = NULL;\n+\n+\tif (_clar.report_errors_only) {\n+\t\tclar_report_errors(_clar.last_report);\n+\t} else {\n+\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n+\t}\n+}\n+\n+static void\n+clar_run_suite(const struct clar_suite *suite, const char *filter)\n+{\n+\tconst struct clar_func *test = suite->tests;\n+\tsize_t i, matchlen;\n+\tstruct clar_report *report;\n+\tint exact = 0;\n+\n+\tif (!suite->enabled)\n+\t\treturn;\n+\n+\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\treturn;\n+\n+\tif (!_clar.report_errors_only)\n+\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n+\n+\t_clar.active_suite = suite->name;\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n+\n+\tif (filter) {\n+\t\tsize_t suitelen = strlen(suite->name);\n+\t\tmatchlen = strlen(filter);\n+\t\tif (matchlen <= suitelen) {\n+\t\t\tfilter = NULL;\n+\t\t} else {\n+\t\t\tfilter += suitelen;\n+\t\t\twhile (*filter == ':')\n+\t\t\t\t++filter;\n+\t\t\tmatchlen = strlen(filter);\n+\n+\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n+\t\t\t\texact = 1;\n+\t\t\t\tmatchlen--;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (i = 0; i < suite->test_count; ++i) {\n+\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n+\t\t\tcontinue;\n+\n+\t\tif (exact && strlen(test[i].name) != matchlen)\n+\t\t\tcontinue;\n+\n+\t\t_clar.active_test = test[i].name;\n+\n+\t\treport = calloc(1, sizeof(struct clar_report));\n+\t\treport->suite = _clar.active_suite;\n+\t\treport->test = _clar.active_test;\n+\t\treport->test_number = _clar.tests_ran;\n+\t\treport->status = CL_TEST_NOTRUN;\n+\n+\t\tif (_clar.reports == NULL)\n+\t\t\t_clar.reports = report;\n+\n+\t\tif (_clar.last_report != NULL)\n+\t\t\t_clar.last_report->next = report;\n+\n+\t\t_clar.last_report = report;\n+\n+\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n+\n+\t\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\t\treturn;\n+\t}\n+\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_END);\n+}\n+\n+static void\n+clar_usage(const char *arg)\n+{\n+\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n+\tprintf(\"Options:\\n\");\n+\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n+\tprintf(\"  -iname        Include the suite with `name`\\n\");\n+\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n+\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n+\tprintf(\"  -q            Only report tests that had an error\\n\");\n+\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n+\tprintf(\"  -t            Display results in tap format\\n\");\n+\tprintf(\"  -l            Print suite names\\n\");\n+\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n+\texit(-1);\n+}\n+\n+static void\n+clar_parse_args(int argc, char **argv)\n+{\n+\tint i;\n+\n+\t/* Verify options before execute */\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tif (argument[0] != '-' || argument[1] == '\\0'\n+\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n+\t\t\tclar_usage(argv[0]);\n+\t\t}\n+\t}\n+\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tswitch (argument[1]) {\n+\t\tcase 's':\n+\t\tcase 'i':\n+\t\tcase 'x': { /* given suite name */\n+\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n+\t\t\tchar action = argument[1];\n+\t\t\tsize_t j, arglen, suitelen, cmplen;\n+\n+\t\t\targument += offset;\n+\t\t\targlen = strlen(argument);\n+\n+\t\t\tif (arglen == 0)\n+\t\t\t\tclar_usage(argv[0]);\n+\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n+\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n+\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n+\n+\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n+\t\t\t\t\tint exact = (arglen >= suitelen);\n+\n+\t\t\t\t\t/* Do we have a real suite prefix separated by a\n+\t\t\t\t\t * trailing '::' or just a matching substring? */\n+\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n+\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n+\t\t\t\t\t    continue;\n+\n+\t\t\t\t\t++found;\n+\n+\t\t\t\t\tif (!exact)\n+\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n+\n+\t\t\t\t\tswitch (action) {\n+\t\t\t\t\tcase 's': {\n+\t\t\t\t\t\tstruct clar_explicit *explicit =\n+\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n+\t\t\t\t\t\tassert(explicit);\n+\n+\t\t\t\t\t\texplicit->suite_idx = j;\n+\t\t\t\t\t\texplicit->filter = argument;\n+\n+\t\t\t\t\t\tif (_clar.explicit == NULL)\n+\t\t\t\t\t\t\t_clar.explicit = explicit;\n+\n+\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n+\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n+\n+\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n+\t\t\t\t\t\t_clar.last_explicit = explicit;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n+\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif (exact)\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (!found) {\n+\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n+\t\t\t\texit(-1);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase 'q':\n+\t\t\t_clar.report_errors_only = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 'Q':\n+\t\t\t_clar.exit_on_error = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 't':\n+\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n+\t\t\tbreak;\n+\n+\t\tcase 'l': {\n+\t\t\tsize_t j;\n+\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n+\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n+\n+\t\t\texit(0);\n+\t\t}\n+\n+\t\tcase 'v':\n+\t\t\t_clar.verbosity++;\n+\t\t\tbreak;\n+\n+\t\tcase 'r':\n+\t\t\t_clar.write_summary = 1;\n+\t\t\tfree(_clar.summary_filename);\n+\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tassert(!\"Unexpected commandline argument!\");\n+\t\t}\n+\t}\n+}\n+\n+void\n+clar_test_init(int argc, char **argv)\n+{\n+\tconst char *summary_env;\n+\n+\tif (argc > 1)\n+\t\tclar_parse_args(argc, argv);\n+\n+\tclar_print_init(\n+\t\t(int)_clar_callback_count,\n+\t\t(int)_clar_suite_count,\n+\t\t\"\"\n+\t);\n+\n+\tif (!_clar.summary_filename &&\n+\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n+\t\t_clar.write_summary = 1;\n+\t\t_clar.summary_filename = strdup(summary_env);\n+\t}\n+\n+\tif (_clar.write_summary && !_clar.summary_filename)\n+\t\t_clar.summary_filename = strdup(\"summary.xml\");\n+\n+\tif (_clar.write_summary &&\n+\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n+\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tif (clar_sandbox() < 0) {\n+\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n+\t\texit(-1);\n+\t}\n+}\n+\n+int\n+clar_test_run(void)\n+{\n+\tsize_t i;\n+\tstruct clar_explicit *explicit;\n+\n+\tif (_clar.explicit) {\n+\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n+\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n+\t} else {\n+\t\tfor (i = 0; i < _clar_suite_count; ++i)\n+\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n+\t}\n+\n+\treturn _clar.total_errors;\n+}\n+\n+void\n+clar_test_shutdown(void)\n+{\n+\tstruct clar_explicit *explicit, *explicit_next;\n+\tstruct clar_report *report, *report_next;\n+\n+\tclar_print_shutdown(\n+\t\t_clar.tests_ran,\n+\t\t(int)_clar_suite_count,\n+\t\t_clar.total_errors\n+\t);\n+\n+\tclar_unsandbox();\n+\n+\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n+\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n+\t\texplicit_next = explicit->next;\n+\t\tfree(explicit);\n+\t}\n+\n+\tfor (report = _clar.reports; report; report = report_next) {\n+\t\treport_next = report->next;\n+\t\tfree(report);\n+\t}\n+\n+\tfree(_clar.summary_filename);\n+}\n+\n+int\n+clar_test(int argc, char **argv)\n+{\n+\tint errors;\n+\n+\tclar_test_init(argc, argv);\n+\terrors = clar_test_run();\n+\tclar_test_shutdown();\n+\n+\treturn errors;\n+}\n+\n+static void abort_test(void)\n+{\n+\tif (!_clar.trampoline_enabled) {\n+\t\tclar_print_onabort(\n+\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n+\t\tclar_report_errors(_clar.last_report);\n+\t\texit(-1);\n+\t}\n+\n+\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n+\tlongjmp(_clar.trampoline, -1);\n+}\n+\n+void clar__skip(void)\n+{\n+\t_clar.last_report->status = CL_TEST_SKIP;\n+\t_clar.total_skipped++;\n+\tabort_test();\n+}\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n+\n+\tif (_clar.last_report->errors == NULL)\n+\t\t_clar.last_report->errors = error;\n+\n+\tif (_clar.last_report->last_error != NULL)\n+\t\t_clar.last_report->last_error->next = error;\n+\n+\t_clar.last_report->last_error = error;\n+\n+\terror->file = file;\n+\terror->function = function;\n+\terror->line_number = line;\n+\terror->error_msg = error_msg;\n+\n+\tif (description != NULL)\n+\t\terror->description = strdup(description);\n+\n+\t_clar.total_errors++;\n+\t_clar.last_report->status = CL_TEST_FAILURE;\n+\n+\tif (should_abort)\n+\t\tabort_test();\n+}\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tif (condition)\n+\t\treturn;\n+\n+\tclar__fail(file, function, line, error_msg, description, should_abort);\n+}\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...)\n+{\n+\tva_list args;\n+\tchar buf[4096];\n+\tint is_equal = 1;\n+\n+\tva_start(args, fmt);\n+\n+\tif (!strcmp(\"%s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n+\t\t\t\t\ts1, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n+\t\t\t\t\tlen, s1, len, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n+\t\t\t\t\twcs1, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n+\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n+\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n+\t\tis_equal = (sz1 == sz2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%p\", fmt)) {\n+\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n+\t\tis_equal = (p1 == p2);\n+\t\tif (!is_equal)\n+\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n+\t}\n+\telse {\n+\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n+\t\tis_equal = (i1 == i2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n+\t\t}\n+\t}\n+\n+\tva_end(args);\n+\n+\tif (!is_equal)\n+\t\tclar__fail(file, function, line, err, buf, should_abort);\n+}\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n+{\n+\t_clar.local_cleanup = cleanup;\n+\t_clar.local_cleanup_payload = opaque;\n+}\n+\n+#include \"clar/sandbox.h\"\n+#include \"clar/fixtures.h\"\n+#include \"clar/fs.h\"\n+#include \"clar/print.h\"\n+#include \"clar/summary.h\"\ndiff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\nnew file mode 100644\nindex 00000000000..8c22382bd56\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.h\n@@ -0,0 +1,173 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST_H__\n+#define __CLAR_TEST_H__\n+\n+#include <stdlib.h>\n+\n+enum cl_test_status {\n+\tCL_TEST_OK,\n+\tCL_TEST_FAILURE,\n+\tCL_TEST_SKIP,\n+\tCL_TEST_NOTRUN,\n+};\n+\n+enum cl_output_format {\n+\tCL_OUTPUT_CLAP,\n+\tCL_OUTPUT_TAP,\n+};\n+\n+/** Setup clar environment */\n+void clar_test_init(int argc, char *argv[]);\n+int clar_test_run(void);\n+void clar_test_shutdown(void);\n+\n+/** One shot setup & run */\n+int clar_test(int argc, char *argv[]);\n+\n+const char *clar_sandbox_path(void);\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n+void cl_fs_cleanup(void);\n+\n+/**\n+ * cl_trace_* is a hook to provide a simple global tracing\n+ * mechanism.\n+ *\n+ * The goal here is to let main() provide clar-proper\n+ * with a callback to optionally write log info for\n+ * test operations into the same stream used by their\n+ * actual tests.  This would let them print test names\n+ * and maybe performance data as they choose.\n+ *\n+ * The goal is NOT to alter the flow of control or to\n+ * override test selection/skipping.  (So the callback\n+ * does not return a value.)\n+ *\n+ * The goal is NOT to duplicate the existing\n+ * pass/fail/skip reporting.  (So the callback\n+ * does not accept a status/errorcode argument.)\n+ *\n+ */\n+typedef enum cl_trace_event {\n+\tCL_TRACE__SUITE_BEGIN,\n+\tCL_TRACE__SUITE_END,\n+\tCL_TRACE__TEST__BEGIN,\n+\tCL_TRACE__TEST__END,\n+\tCL_TRACE__TEST__RUN_BEGIN,\n+\tCL_TRACE__TEST__RUN_END,\n+\tCL_TRACE__TEST__LONGJMP,\n+} cl_trace_event;\n+\n+typedef void (cl_trace_cb)(\n+\tcl_trace_event ev,\n+\tconst char *suite_name,\n+\tconst char *test_name,\n+\tvoid *payload);\n+\n+/**\n+ * Register a callback into CLAR to send global trace events.\n+ * Pass NULL to disable.\n+ */\n+void cl_trace_register(cl_trace_cb *cb, void *payload);\n+\n+\n+#ifdef CLAR_FIXTURE_PATH\n+const char *cl_fixture(const char *fixture_name);\n+void cl_fixture_sandbox(const char *fixture_name);\n+void cl_fixture_cleanup(const char *fixture_name);\n+const char *cl_fixture_basename(const char *fixture_name);\n+#endif\n+\n+/**\n+ * Assertion macros with explicit error message\n+ */\n+#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n+#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n+#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n+\n+/**\n+ * Check macros with explicit error message\n+ */\n+#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n+#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n+#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n+\n+/**\n+ * Assertion macros with no error message\n+ */\n+#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n+#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n+#define cl_assert(expr) cl_assert_(expr, NULL)\n+\n+/**\n+ * Check macros with no error message\n+ */\n+#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n+#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n+#define cl_check(expr) cl_check_(expr, NULL)\n+\n+/**\n+ * Forced failure/warning\n+ */\n+#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n+#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n+\n+#define cl_skip() clar__skip()\n+\n+/**\n+ * Typed assertion macros\n+ */\n+#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n+#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n+\n+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n+\n+#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n+#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n+\n+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+\n+#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n+#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n+#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n+\n+#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n+\n+#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n+\n+void clar__skip(void);\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...);\n+\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\nnew file mode 100644\nindex 00000000000..6ec6423484d\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fixtures.h\n@@ -0,0 +1,50 @@\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name)\n+{\n+\tstatic char _path[4096];\n+\tsize_t root_len;\n+\n+\troot_len = strlen(base);\n+\tstrncpy(_path, base, sizeof(_path));\n+\n+\tif (_path[root_len - 1] != '/')\n+\t\t_path[root_len++] = '/';\n+\n+\tif (fixture_name[0] == '/')\n+\t\tfixture_name++;\n+\n+\tstrncpy(_path + root_len,\n+\t\tfixture_name,\n+\t\tsizeof(_path) - root_len);\n+\n+\treturn _path;\n+}\n+\n+const char *cl_fixture(const char *fixture_name)\n+{\n+\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n+}\n+\n+void cl_fixture_sandbox(const char *fixture_name)\n+{\n+\tfs_copy(cl_fixture(fixture_name), _clar_path);\n+}\n+\n+const char *cl_fixture_basename(const char *fixture_name)\n+{\n+\tconst char *p;\n+\n+\tfor (p = fixture_name; *p; p++) {\n+\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n+\t\t\tfixture_name = p+1;\n+\t}\n+\n+\treturn fixture_name;\n+}\n+\n+void cl_fixture_cleanup(const char *fixture_name)\n+{\n+\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nnew file mode 100644\nindex 00000000000..3e39890bd3e\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -0,0 +1,522 @@\n+/*\n+ * By default, use a read/write loop to copy files on POSIX systems.\n+ * On Linux, use sendfile by default as it's slightly faster.  On\n+ * macOS, we avoid fcopyfile by default because it's slightly slower.\n+ */\n+#undef USE_FCOPYFILE\n+#define USE_SENDFILE 1\n+\n+#ifdef _WIN32\n+\n+#ifdef CLAR_WIN32_LONGPATHS\n+# define CLAR_MAX_PATH 4096\n+#else\n+# define CLAR_MAX_PATH MAX_PATH\n+#endif\n+\n+#define RM_RETRY_COUNT\t5\n+#define RM_RETRY_DELAY\t10\n+\n+#ifdef __MINGW32__\n+\n+/* These security-enhanced functions are not available\n+ * in MinGW, so just use the vanilla ones */\n+#define wcscpy_s(a, b, c) wcscpy((a), (c))\n+#define wcscat_s(a, b, c) wcscat((a), (c))\n+\n+#endif /* __MINGW32__ */\n+\n+static int\n+fs__dotordotdot(WCHAR *_tocheck)\n+{\n+\treturn _tocheck[0] == '.' &&\n+\t\t(_tocheck[1] == '\\0' ||\n+\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n+}\n+\n+static int\n+fs_rmdir_rmdir(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\n+\twhile (!RemoveDirectoryW(_wpath)) {\n+\t\t/* Only retry when we have retries remaining, and the\n+\t\t * error was ERROR_DIR_NOT_EMPTY. */\n+\t\tif (retries++ > RM_RETRY_COUNT ||\n+\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n+\t\t\treturn -1;\n+\n+\t\t/* Give whatever has a handle to a child item some time\n+\t\t * to release it before trying again */\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void translate_path(WCHAR *path, size_t path_size)\n+{\n+    size_t path_len, i;\n+\n+    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n+\treturn;\n+\n+    path_len = wcslen(path);\n+    cl_assert(path_size > path_len + 4);\n+\n+    for (i = path_len; i > 0; i--) {\n+\tWCHAR c = path[i - 1];\n+\n+\tif (c == L'/')\n+\t    path[i + 3] = L'\\\\';\n+\telse\n+\t    path[i + 3] = path[i - 1];\n+    }\n+\n+    path[0] = L'\\\\';\n+    path[1] = L'\\\\';\n+    path[2] = L'?';\n+    path[3] = L'\\\\';\n+    path[path_len + 4] = L'\\0';\n+}\n+\n+static void\n+fs_rmdir_helper(WCHAR *_wsource)\n+{\n+\tWCHAR buffer[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buffer_prefix_len;\n+\n+\t/* Set up the buffer and capture the length */\n+\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n+\ttranslate_path(buffer, CLAR_MAX_PATH);\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n+\tbuffer_prefix_len = wcslen(buffer);\n+\n+\t/* FindFirstFile needs a wildcard to match multiple items */\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buffer, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_rmdir_helper(buffer);\n+\t\telse {\n+\t\t\t/* If set, the +R bit must be cleared before deleting */\n+\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n+\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\t\tcl_assert(DeleteFileW(buffer));\n+\t\t}\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+\n+\t/* Now that the directory is empty, remove it */\n+\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n+}\n+\n+static int\n+fs_rm_wait(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\tDWORD last_error;\n+\n+\tdo {\n+\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n+\t\t\tlast_error = GetLastError();\n+\t\telse\n+\t\t\tlast_error = ERROR_SUCCESS;\n+\n+\t\t/* Is the item gone? */\n+\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n+\t\t\tERROR_PATH_NOT_FOUND == last_error)\n+\t\t\treturn 0;\n+\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\twhile (retries++ <= RM_RETRY_COUNT);\n+\n+\treturn -1;\n+}\n+\n+static void\n+fs_rm(const char *_source)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH];\n+\tDWORD attrs;\n+\n+\t/* The input path is UTF-8. Convert it to wide characters\n+\t * for use with the Windows API */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1, /* Indicates NULL termination */\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\n+\t/* Does the item exist? If not, we have no work to do */\n+\tattrs = GetFileAttributesW(wsource);\n+\n+\tif (INVALID_FILE_ATTRIBUTES == attrs)\n+\t\treturn;\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n+\t\tfs_rmdir_helper(wsource);\n+\telse {\n+\t\t/* The item is a file. Strip the +R bit */\n+\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n+\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\tcl_assert(DeleteFileW(wsource));\n+\t}\n+\n+\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n+\tcl_assert(0 == fs_rm_wait(wsource));\n+}\n+\n+static void\n+fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n+{\n+\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n+\n+\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_source, CLAR_MAX_PATH);\n+\tbuf_source_prefix_len = wcslen(buf_source);\n+\n+\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n+\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n+\tbuf_dest_prefix_len = wcslen(buf_dest);\n+\n+\t/* Get an enumerator for the items in the source. */\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buf_source, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\t/* Create the target directory. */\n+\tcl_assert(CreateDirectoryW(_wdest, NULL));\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n+\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_copydir_helper(buf_source, buf_dest);\n+\t\telse\n+\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+}\n+\n+static void\n+fs_copy(const char *_source, const char *_dest)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n+\tDWORD source_attrs, dest_attrs;\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\n+\t/* The input paths are UTF-8. Convert them to wide characters\n+\t * for use with the Windows API. */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1,\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_dest,\n+\t\t\t\t-1,\n+\t\t\t\twdest,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\ttranslate_path(wdest, CLAR_MAX_PATH);\n+\n+\t/* Check the source for existence */\n+\tsource_attrs = GetFileAttributesW(wsource);\n+\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n+\n+\t/* Check the target for existence */\n+\tdest_attrs = GetFileAttributesW(wdest);\n+\n+\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n+\t\t/* Target exists; append last path part of source to target.\n+\t\t * Use FindFirstFile to parse the path */\n+\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n+\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n+\t\tFindClose(find_handle);\n+\n+\t\t/* Check the new target for existence */\n+\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n+\t}\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n+\t\tfs_copydir_helper(wsource, wdest);\n+\telse\n+\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+#ifdef CLAR_FIXTURE_PATH\n+\tfs_rm(fixture_path(_clar_path, \"*\"));\n+#endif\n+}\n+\n+#else\n+\n+#include <errno.h>\n+#include <string.h>\n+#include <limits.h>\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#if defined(__linux__)\n+# include <sys/sendfile.h>\n+#endif\n+\n+#if defined(__APPLE__)\n+# include <copyfile.h>\n+#endif\n+\n+static void basename_r(const char **out, int *out_len, const char *in)\n+{\n+\tsize_t in_len = strlen(in), start_pos;\n+\n+\tfor (in_len = strlen(in); in_len; in_len--) {\n+\t\tif (in[in_len - 1] != '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tfor (start_pos = in_len; start_pos; start_pos--) {\n+\t\tif (in[start_pos - 1] == '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tcl_assert(in_len - start_pos < INT_MAX);\n+\n+\tif (in_len - start_pos > 0) {\n+\t\t*out = &in[start_pos];\n+\t\t*out_len = (in_len - start_pos);\n+\t} else {\n+\t\t*out = \"/\";\n+\t\t*out_len = 1;\n+\t}\n+}\n+\n+static char *joinpath(const char *dir, const char *base, int base_len)\n+{\n+\tchar *out;\n+\tint len;\n+\n+\tif (base_len == -1) {\n+\t\tsize_t bl = strlen(base);\n+\n+\t\tcl_assert(bl < INT_MAX);\n+\t\tbase_len = (int)bl;\n+\t}\n+\n+\tlen = strlen(dir) + base_len + 2;\n+\tcl_assert(len > 0);\n+\n+\tcl_assert(out = malloc(len));\n+\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n+\n+\treturn out;\n+}\n+\n+static void\n+fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n+{\n+\tDIR *source_dir;\n+\tstruct dirent *d;\n+\n+\tmkdir(dest, dest_mode);\n+\n+\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n+\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(source, d->d_name, -1);\n+\t\tfs_copy(child, dest);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\n+\tclosedir(source_dir);\n+}\n+\n+static void\n+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n+{\n+\tint in, out;\n+\n+\tcl_must_pass((in = open(source, O_RDONLY)));\n+\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n+\n+#if USE_FCOPYFILE && defined(__APPLE__)\n+\t((void)(source_len)); /* unused */\n+\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n+#elif USE_SENDFILE && defined(__linux__)\n+\t{\n+\t\tssize_t ret = 0;\n+\n+\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n+\t\t\tsource_len -= (size_t)ret;\n+\t\t}\n+\t\tcl_assert(ret >= 0);\n+\t}\n+#else\n+\t{\n+\t\tchar buf[131072];\n+\t\tssize_t ret;\n+\n+\t\t((void)(source_len)); /* unused */\n+\n+\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n+\t\t\tsize_t len = (size_t)ret;\n+\n+\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n+\t\t\t\tcl_assert(ret <= (ssize_t)len);\n+\t\t\t\tlen -= ret;\n+\t\t\t}\n+\t\t\tcl_assert(ret >= 0);\n+\t\t}\n+\t\tcl_assert(ret == 0);\n+\t}\n+#endif\n+\n+\tclose(in);\n+\tclose(out);\n+}\n+\n+static void\n+fs_copy(const char *source, const char *_dest)\n+{\n+\tchar *dbuf = NULL;\n+\tconst char *dest = NULL;\n+\tstruct stat source_st, dest_st;\n+\n+\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n+\n+\tif (lstat(_dest, &dest_st) == 0) {\n+\t\tconst char *base;\n+\t\tint base_len;\n+\n+\t\t/* Target exists and is directory; append basename */\n+\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n+\n+\t\tbasename_r(&base, &base_len, source);\n+\t\tcl_assert(base_len < INT_MAX);\n+\n+\t\tdbuf = joinpath(_dest, base, base_len);\n+\t\tdest = dbuf;\n+\t} else if (errno != ENOENT) {\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t} else {\n+\t\tdest = _dest;\n+\t}\n+\n+\tif (S_ISDIR(source_st.st_mode)) {\n+\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n+\t} else {\n+\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n+\t}\n+\n+\tfree(dbuf);\n+}\n+\n+static void\n+fs_rmdir_helper(const char *path)\n+{\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n+\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(path, d->d_name, -1);\n+\t\tfs_rm(child);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\tclosedir(dir);\n+\n+\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n+}\n+\n+static void\n+fs_rm(const char *path)\n+{\n+\tstruct stat st;\n+\n+\tif (lstat(path, &st)) {\n+\t\tif (errno == ENOENT)\n+\t\t\treturn;\n+\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t}\n+\n+\tif (S_ISDIR(st.st_mode)) {\n+\t\tfs_rmdir_helper(path);\n+\t} else {\n+\t\tcl_must_pass(unlink(path));\n+\t}\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+\tclar_unsandbox();\n+\tclar_sandbox();\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\nnew file mode 100644\nindex 00000000000..c17e2f693bd\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/print.h\n@@ -0,0 +1,211 @@\n+/* clap: clar protocol, the traditional clar output format */\n+\n+static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n+\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n+}\n+\n+static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"\\n\\n\");\n+\tclar_report_all();\n+}\n+\n+static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tprintf(\"  %d) Failure:\\n\", num);\n+\n+\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n+\t\treport->suite,\n+\t\treport->test,\n+\t\terror->file,\n+\t\terror->line_number);\n+\n+\tprintf(\"  %s\\n\", error->error_msg);\n+\n+\tif (error->description != NULL)\n+\t\tprintf(\"  %s\\n\", error->description);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tif (_clar.verbosity > 1) {\n+\t\tprintf(\"%s::%s: \", suite_name, test_name);\n+\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n+\t\t}\n+\t} else {\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\".\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n+\t\t}\n+\n+\t\tfflush(stdout);\n+\t}\n+}\n+\n+static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tif (_clar.verbosity == 1)\n+\t\tprintf(\"\\n%s\", suite_name);\n+\n+\t(void)suite_index;\n+}\n+\n+static void clar_print_clap_onabort(const char *fmt, va_list arg)\n+{\n+\tvfprintf(stderr, fmt, arg);\n+}\n+\n+/* tap: test anywhere protocol format */\n+\n+static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)suite_names;\n+\tprintf(\"TAP version 13\\n\");\n+}\n+\n+static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"1..%d\\n\", test_count);\n+}\n+\n+static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\t(void)num;\n+\t(void)report;\n+\t(void)error;\n+}\n+\n+static void print_escaped(const char *str)\n+{\n+\tchar *c;\n+\n+\twhile ((c = strchr(str, '\\'')) != NULL) {\n+\t\tprintf(\"%.*s\", (int)(c - str), str);\n+\t\tprintf(\"''\");\n+\t\tstr = c + 1;\n+\t}\n+\n+\tprintf(\"%s\", str);\n+}\n+\n+static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tconst struct clar_error *error = _clar.last_report->errors;\n+\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tswitch(status) {\n+\tcase CL_TEST_OK:\n+\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\tcase CL_TEST_FAILURE:\n+\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\n+\t\tprintf(\"    ---\\n\");\n+\t\tprintf(\"    reason: |\\n\");\n+\t\tprintf(\"      %s\\n\", error->error_msg);\n+\n+\t\tif (error->description)\n+\t\t\tprintf(\"      %s\\n\", error->description);\n+\n+\t\tprintf(\"    at:\\n\");\n+\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n+\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n+\t\tprintf(\"      function: '%s'\\n\", error->function);\n+\t\tprintf(\"    ---\\n\");\n+\n+\t\tbreak;\n+\tcase CL_TEST_SKIP:\n+\tcase CL_TEST_NOTRUN:\n+\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\t}\n+\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n+}\n+\n+static void clar_print_tap_onabort(const char *fmt, va_list arg)\n+{\n+\tprintf(\"Bail out! \");\n+\tvprintf(fmt, arg);\n+\tfflush(stdout);\n+}\n+\n+/* indirection between protocol output selection */\n+\n+#define PRINT(FN, ...) do { \\\n+\t\tswitch (_clar.output_format) { \\\n+\t\t\tcase CL_OUTPUT_CLAP: \\\n+\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tcase CL_OUTPUT_TAP: \\\n+\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tdefault: \\\n+\t\t\t\tabort(); \\\n+\t\t} \\\n+\t} while (0)\n+\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\tPRINT(init, test_count, suite_count, suite_names);\n+}\n+\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\tPRINT(shutdown, test_count, suite_count, error_count);\n+}\n+\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tPRINT(error, num, report, error);\n+}\n+\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tPRINT(ontest, suite_name, test_name, test_number, status);\n+}\n+\n+static void clar_print_onsuite(const char *suite_name, int suite_index)\n+{\n+\tPRINT(onsuite, suite_name, suite_index);\n+}\n+\n+static void clar_print_onabort(const char *msg, ...)\n+{\n+\tva_list argp;\n+\tva_start(argp, msg);\n+\tPRINT(onabort, msg, argp);\n+\tva_end(argp);\n+}\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nnew file mode 100644\nindex 00000000000..7c177f35258\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -0,0 +1,153 @@\n+#ifdef __APPLE__\n+#include <sys/syslimits.h>\n+#endif\n+\n+static char _clar_path[4096 + 1];\n+\n+static int\n+is_valid_tmp_path(const char *path)\n+{\n+\tSTAT_T st;\n+\n+\tif (stat(path, &st) != 0)\n+\t\treturn 0;\n+\n+\tif (!S_ISDIR(st.st_mode))\n+\t\treturn 0;\n+\n+\treturn (access(path, W_OK) == 0);\n+}\n+\n+static int\n+find_tmp_path(char *buffer, size_t length)\n+{\n+#ifndef _WIN32\n+\tstatic const size_t var_count = 5;\n+\tstatic const char *env_vars[] = {\n+\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n+\t};\n+\n+\tsize_t i;\n+\n+\tfor (i = 0; i < var_count; ++i) {\n+\t\tconst char *env = getenv(env_vars[i]);\n+\t\tif (!env)\n+\t\t\tcontinue;\n+\n+\t\tif (is_valid_tmp_path(env)) {\n+#ifdef __APPLE__\n+\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n+\t\t\t\treturn 0;\n+#endif\n+\t\t\tstrncpy(buffer, env, length - 1);\n+\t\t\tbuffer[length - 1] = '\\0';\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\t/* If the environment doesn't say anything, try to use /tmp */\n+\tif (is_valid_tmp_path(\"/tmp\")) {\n+#ifdef __APPLE__\n+\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n+\t\t\treturn 0;\n+#endif\n+\t\tstrncpy(buffer, \"/tmp\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+#else\n+\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n+\tif (env_len > 0 && env_len < (DWORD)length)\n+\t\treturn 0;\n+\n+\tif (GetTempPath((DWORD)length, buffer))\n+\t\treturn 0;\n+#endif\n+\n+\t/* This system doesn't like us, try to use the current directory */\n+\tif (is_valid_tmp_path(\".\")) {\n+\t\tstrncpy(buffer, \".\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static void clar_unsandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0')\n+\t\treturn;\n+\n+\tcl_must_pass(chdir(\"..\"));\n+\n+\tfs_rm(_clar_path);\n+}\n+\n+static int build_sandbox_path(void)\n+{\n+#ifdef CLAR_TMPDIR\n+\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n+#else\n+\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n+#endif\n+\n+\tsize_t len;\n+\n+\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n+\t\treturn -1;\n+\n+\tlen = strlen(_clar_path);\n+\n+#ifdef _WIN32\n+\t{ /* normalize path to POSIX forward slashes */\n+\t\tsize_t i;\n+\t\tfor (i = 0; i < len; ++i) {\n+\t\t\tif (_clar_path[i] == '\\\\')\n+\t\t\t\t_clar_path[i] = '/';\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (_clar_path[len - 1] != '/') {\n+\t\t_clar_path[len++] = '/';\n+\t}\n+\n+\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n+\n+#if defined(__MINGW32__)\n+\tif (_mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(_WIN32)\n+\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#else\n+\tif (mkdtemp(_clar_path) == NULL)\n+\t\treturn -1;\n+#endif\n+\n+\treturn 0;\n+}\n+\n+static int clar_sandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n+\t\treturn -1;\n+\n+\tif (chdir(_clar_path) != 0)\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const char *clar_sandbox_path(void)\n+{\n+\treturn _clar_path;\n+}\ndiff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\nnew file mode 100644\nindex 00000000000..4dd352e28b8\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/summary.h\n@@ -0,0 +1,143 @@\n+\n+#include <stdio.h>\n+#include <time.h>\n+\n+static int clar_summary_close_tag(\n+    struct clar_summary *summary, const char *tag, int indent)\n+{\n+\tconst char *indt;\n+\n+\tif (indent == 0) indt = \"\";\n+\telse if (indent == 1) indt = \"\\t\";\n+\telse indt = \"\\t\\t\";\n+\n+\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n+}\n+\n+static int clar_summary_testsuites(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n+}\n+\n+static int clar_summary_testsuite(struct clar_summary *summary,\n+    int idn, const char *name, time_t timestamp,\n+    int test_count, int fail_count, int error_count)\n+{\n+\tstruct tm *tm = localtime(&timestamp);\n+\tchar iso_dt[20];\n+\n+\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n+\t\treturn -1;\n+\n+\treturn fprintf(summary->fp, \"\\t<testsuite\"\n+\t\t       \" id=\\\"%d\\\"\"\n+\t\t       \" name=\\\"%s\\\"\"\n+\t\t       \" hostname=\\\"localhost\\\"\"\n+\t\t       \" timestamp=\\\"%s\\\"\"\n+\t\t       \" tests=\\\"%d\\\"\"\n+\t\t       \" failures=\\\"%d\\\"\"\n+\t\t       \" errors=\\\"%d\\\">\\n\",\n+\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n+}\n+\n+static int clar_summary_testcase(struct clar_summary *summary,\n+    const char *name, const char *classname, double elapsed)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n+\t\tname, classname, elapsed);\n+}\n+\n+static int clar_summary_failure(struct clar_summary *summary,\n+    const char *type, const char *message, const char *desc)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n+\t    type, message, desc);\n+}\n+\n+static int clar_summary_skipped(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n+}\n+\n+struct clar_summary *clar_summary_init(const char *filename)\n+{\n+\tstruct clar_summary *summary;\n+\tFILE *fp;\n+\n+\tif ((fp = fopen(filename, \"w\")) == NULL) {\n+\t\tperror(\"fopen\");\n+\t\treturn NULL;\n+\t}\n+\n+\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n+\t\tperror(\"malloc\");\n+\t\tfclose(fp);\n+\t\treturn NULL;\n+\t}\n+\n+\tsummary->filename = filename;\n+\tsummary->fp = fp;\n+\n+\treturn summary;\n+}\n+\n+int clar_summary_shutdown(struct clar_summary *summary)\n+{\n+\tstruct clar_report *report;\n+\tconst char *last_suite = NULL;\n+\n+\tif (clar_summary_testsuites(summary) < 0)\n+\t\tgoto on_error;\n+\n+\treport = _clar.reports;\n+\twhile (report != NULL) {\n+\t\tstruct clar_error *error = report->errors;\n+\n+\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n+\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\n+\t\tlast_suite = report->suite;\n+\n+\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n+\n+\t\twhile (error != NULL) {\n+\t\t\tif (clar_summary_failure(summary, \"assert\",\n+\t\t\t    error->error_msg, error->description) < 0)\n+\t\t\t\tgoto on_error;\n+\n+\t\t\terror = error->next;\n+\t\t}\n+\n+\t\tif (report->status == CL_TEST_SKIP)\n+\t\t\tclar_summary_skipped(summary);\n+\n+\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n+\t\t\tgoto on_error;\n+\n+\t\treport = report->next;\n+\n+\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\t}\n+\n+\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n+\t    fclose(summary->fp) != 0)\n+\t\tgoto on_error;\n+\n+\tprintf(\"written summary file to %s\\n\", summary->filename);\n+\n+\tfree(summary);\n+\treturn 0;\n+\n+on_error:\n+\tfclose(summary->fp);\n+\tfree(summary);\n+\treturn -1;\n+}\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nnew file mode 100755\nindex 00000000000..80996ac3e71\n--- /dev/null\n+++ b/t/unit-tests/clar/generate.py\n@@ -0,0 +1,266 @@\n+#!/usr/bin/env python\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+from __future__ import with_statement\n+from string import Template\n+import re, fnmatch, os, sys, codecs, pickle\n+\n+class Module(object):\n+    class Template(object):\n+        def __init__(self, module):\n+            self.module = module\n+\n+        def _render_callback(self, cb):\n+            if not cb:\n+                return '    { NULL, NULL }'\n+            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n+\n+    class DeclarationTemplate(Template):\n+        def render(self):\n+            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n+\n+            for initializer in self.module.initializers:\n+                out += \"extern %s;\\n\" % initializer['declaration']\n+\n+            if self.module.cleanup:\n+                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n+\n+            return out\n+\n+    class CallbacksTemplate(Template):\n+        def render(self):\n+            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n+            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n+            out += \"\\n};\\n\"\n+            return out\n+\n+    class InfoTemplate(Template):\n+        def render(self):\n+            templates = []\n+\n+            initializers = self.module.initializers\n+            if len(initializers) == 0:\n+                initializers = [ None ]\n+\n+            for initializer in initializers:\n+                name = self.module.clean_name()\n+                if initializer and initializer['short_name'].startswith('initialize_'):\n+                    variant = initializer['short_name'][len('initialize_'):]\n+                    name += \" (%s)\" % variant.replace('_', ' ')\n+\n+                template = Template(\n+            r\"\"\"\n+    {\n+        \"${clean_name}\",\n+    ${initialize},\n+    ${cleanup},\n+        ${cb_ptr}, ${cb_count}, ${enabled}\n+    }\"\"\"\n+                ).substitute(\n+                    clean_name = name,\n+                    initialize = self._render_callback(initializer),\n+                    cleanup = self._render_callback(self.module.cleanup),\n+                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n+                    cb_count = len(self.module.callbacks),\n+                    enabled = int(self.module.enabled)\n+                )\n+                templates.append(template)\n+\n+            return ','.join(templates)\n+\n+    def __init__(self, name):\n+        self.name = name\n+\n+        self.mtime = None\n+        self.enabled = True\n+        self.modified = False\n+\n+    def clean_name(self):\n+        return self.name.replace(\"_\", \"::\")\n+\n+    def _skip_comments(self, text):\n+        SKIP_COMMENTS_REGEX = re.compile(\n+            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n+            re.DOTALL | re.MULTILINE)\n+\n+        def _replacer(match):\n+            s = match.group(0)\n+            return \"\" if s.startswith('/') else s\n+\n+        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n+\n+    def parse(self, contents):\n+        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n+\n+        contents = self._skip_comments(contents)\n+        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n+\n+        self.callbacks = []\n+        self.initializers = []\n+        self.cleanup = None\n+\n+        for (declaration, symbol, short_name) in regex.findall(contents):\n+            data = {\n+                \"short_name\" : short_name,\n+                \"declaration\" : declaration,\n+                \"symbol\" : symbol\n+            }\n+\n+            if short_name.startswith('initialize'):\n+                self.initializers.append(data)\n+            elif short_name == 'cleanup':\n+                self.cleanup = data\n+            else:\n+                self.callbacks.append(data)\n+\n+        return self.callbacks != []\n+\n+    def refresh(self, path):\n+        self.modified = False\n+\n+        try:\n+            st = os.stat(path)\n+\n+            # Not modified\n+            if st.st_mtime == self.mtime:\n+                return True\n+\n+            self.modified = True\n+            self.mtime = st.st_mtime\n+\n+            with codecs.open(path, encoding='utf-8') as fp:\n+                raw_content = fp.read()\n+\n+        except IOError:\n+            return False\n+\n+        return self.parse(raw_content)\n+\n+class TestSuite(object):\n+\n+    def __init__(self, path, output):\n+        self.path = path\n+        self.output = output\n+\n+    def should_generate(self, path):\n+        if not os.path.isfile(path):\n+            return True\n+\n+        if any(module.modified for module in self.modules.values()):\n+            return True\n+\n+        return False\n+\n+    def find_modules(self):\n+        modules = []\n+        for root, _, files in os.walk(self.path):\n+            module_root = root[len(self.path):]\n+            module_root = [c for c in module_root.split(os.sep) if c]\n+\n+            tests_in_module = fnmatch.filter(files, \"*.c\")\n+\n+            for test_file in tests_in_module:\n+                full_path = os.path.join(root, test_file)\n+                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n+\n+                modules.append((full_path, module_name))\n+\n+        return modules\n+\n+    def load_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        cache = {}\n+\n+        try:\n+            fp = open(path, 'rb')\n+            cache = pickle.load(fp)\n+            fp.close()\n+        except (IOError, ValueError):\n+            pass\n+\n+        return cache\n+\n+    def save_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        with open(path, 'wb') as cache:\n+            pickle.dump(self.modules, cache)\n+\n+    def load(self, force = False):\n+        module_data = self.find_modules()\n+        self.modules = {} if force else self.load_cache()\n+\n+        for path, name in module_data:\n+            if name not in self.modules:\n+                self.modules[name] = Module(name)\n+\n+            if not self.modules[name].refresh(path):\n+                del self.modules[name]\n+\n+    def disable(self, excluded):\n+        for exclude in excluded:\n+            for module in self.modules.values():\n+                name = module.clean_name()\n+                if name.startswith(exclude):\n+                    module.enabled = False\n+                    module.modified = True\n+\n+    def suite_count(self):\n+        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n+\n+    def callback_count(self):\n+        return sum(len(module.callbacks) for module in self.modules.values())\n+\n+    def write(self):\n+        output = os.path.join(self.output, 'clar.suite')\n+\n+        if not self.should_generate(output):\n+            return False\n+\n+        with open(output, 'w') as data:\n+            modules = sorted(self.modules.values(), key=lambda module: module.name)\n+\n+            for module in modules:\n+                t = Module.DeclarationTemplate(module)\n+                data.write(t.render())\n+\n+            for module in modules:\n+                t = Module.CallbacksTemplate(module)\n+                data.write(t.render())\n+\n+            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n+                Module.InfoTemplate(module).render() for module in modules\n+            ) + \"\\n};\\n\"\n+\n+            data.write(suites)\n+\n+            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n+            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n+\n+        self.save_cache()\n+        return True\n+\n+if __name__ == '__main__':\n+    from optparse import OptionParser\n+\n+    parser = OptionParser()\n+    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n+    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n+    parser.add_option('-o', '--output', dest='output')\n+\n+    options, args = parser.parse_args()\n+    if len(args) > 1:\n+        print(\"More than one path given\")\n+        sys.exit(1)\n+\n+    path = args.pop() if args else '.'\n+    output = options.output or path\n+    suite = TestSuite(path, output)\n+    suite.load(options.force)\n+    suite.disable(options.excluded)\n+    if suite.write():\n+        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nnew file mode 100644\nindex 00000000000..a477d0c40ca\n--- /dev/null\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -0,0 +1,4 @@\n+clar.suite\n+.clarcache\n+clar_test\n+*.o\ndiff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\nnew file mode 100644\nindex 00000000000..93c6b2ad32c\n--- /dev/null\n+++ b/t/unit-tests/clar/test/Makefile\n@@ -0,0 +1,39 @@\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+#\n+# Set up the path to the clar sources and to the fixtures directory\n+#\n+# The fixture path needs to be an absolute path so it can be used\n+# even after we have chdir'ed into the test directory while testing.\n+#\n+CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n+TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n+CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n+CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n+\n+CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n+\n+.PHONY: clean\n+\n+# list the objects that go into our test\n+objects = main.o sample.o\n+\n+# build the test executable itself\n+clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n+\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n+\n+# test object files depend on clar macros\n+$(objects) : $(CLAR_PATH)clar.h\n+\n+# build the clar.suite file of test metadata\n+clar.suite:\n+\tpython \"$(CLAR_PATH)generate.py\" .\n+\n+# remove all generated files\n+clean:\n+\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\ndiff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\nnew file mode 100644\nindex 00000000000..0fcaa639aa8\n--- /dev/null\n+++ b/t/unit-tests/clar/test/clar_test.h\n@@ -0,0 +1,16 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST__\n+#define __CLAR_TEST__\n+\n+/* Import the standard clar helper functions */\n+#include \"clar.h\"\n+\n+/* Your custom shared includes / defines here */\n+extern int global_test_counter;\n+\n+#endif\ndiff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\nnew file mode 100644\nindex 00000000000..59e56ad255b\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c\n@@ -0,0 +1,40 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Sample main() for clar tests.\n+ *\n+ * You should write your own main routine for clar tests that does specific\n+ * setup and teardown as necessary for your application.  The only required\n+ * line is the call to `clar_test(argc, argv)`, which will execute the test\n+ * suite.  If you want to check the return value of the test application,\n+ * your main() should return the same value returned by clar_test().\n+ */\n+\n+int global_test_counter = 0;\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\tint ret;\n+\n+\t/* Your custom initialization here */\n+\tglobal_test_counter = 0;\n+\n+\t/* Run the test suite */\n+\tret = clar_test(argc, argv);\n+\n+\t/* Your custom cleanup here */\n+\tcl_assert_equal_i(8, global_test_counter);\n+\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\nnew file mode 100644\nindex 00000000000..a4d91b72fa8\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c.sample\n@@ -0,0 +1,27 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Minimal main() for clar tests.\n+ *\n+ * Modify this with any application specific setup or teardown that you need.\n+ * The only required line is the call to `clar_test(argc, argv)`, which will\n+ * execute the test suite.  If you want to check the return value of the test\n+ * application, main() should return the same value returned by clar_test().\n+ */\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\t/* Run the test suite */\n+\treturn clar_test(argc, argv);\n+}\ndiff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\nnew file mode 100644\nindex 00000000000..220f4aa98a7\n--- /dev/null\n+++ b/t/unit-tests/clar/test/resources/test/file\n@@ -0,0 +1 @@\n+File\ndiff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\nnew file mode 100644\nindex 00000000000..faa1209262f\n--- /dev/null\n+++ b/t/unit-tests/clar/test/sample.c\n@@ -0,0 +1,84 @@\n+#include \"clar_test.h\"\n+#include <sys/stat.h>\n+\n+static int file_size(const char *filename)\n+{\n+\tstruct stat st;\n+\n+\tif (stat(filename, &st) == 0)\n+\t\treturn (int)st.st_size;\n+\treturn -1;\n+}\n+\n+void test_sample__initialize(void)\n+{\n+\tglobal_test_counter++;\n+}\n+\n+void test_sample__cleanup(void)\n+{\n+\tcl_fixture_cleanup(\"test\");\n+\n+\tcl_assert(file_size(\"test/file\") == -1);\n+}\n+\n+void test_sample__1(void)\n+{\n+\tcl_assert(1);\n+\tcl_must_pass(0);  /* 0 == success */\n+\tcl_must_fail(-1); /* <0 == failure */\n+\tcl_must_pass(-1); /* demonstrate a failing call */\n+}\n+\n+void test_sample__2(void)\n+{\n+\tcl_fixture_sandbox(\"test\");\n+\n+\tcl_assert(file_size(\"test/nonexistent\") == -1);\n+\tcl_assert(file_size(\"test/file\") > 0);\n+\tcl_assert(100 == 101);\n+}\n+\n+void test_sample__strings(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_s(\"expected\", actual);\n+\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n+\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n+}\n+\n+void test_sample__strings_with_length(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_strn(\"expected_\", actual, 8);\n+\tcl_assert_equal_strn(\"exactly\", actual, 2);\n+\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n+\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n+}\n+\n+void test_sample__int(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i(100, value);\n+\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n+}\n+\n+void test_sample__int_fmt(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n+}\n+\n+void test_sample__bool(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n+\tcl_assert_equal_b(0, value);\n+}\n+\n+void test_sample__ptr(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n+\tcl_assert_equal_p(&actual, actual);\n+}\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501341","messageId":"5c21aa87aa2496d9f56768b4ce14a2267c763578.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 03/13] t/clar: fix compatibility with NonStop","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:16Z","receivedAt":"2024-08-20T14:02:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The NonStop platform does not have `mkdtemp()` available, which we rely\non in `build_sandbox_path()`. Fix this issue by using `mktemp()` and\n`mkdir()` instead on this platform.\n\nThis has been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/96\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 7c177f35258..e25057b7c49 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -120,6 +120,12 @@ static int build_sandbox_path(void)\n \tif (_mktemp(_clar_path) == NULL)\n \t\treturn -1;\n \n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(__TANDEM)\n+\tif (mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n \tif (mkdir(_clar_path, 0700) != 0)\n \t\treturn -1;\n #elif defined(_WIN32)\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501343","messageId":"06d2bce0d821175be323f352574068962ad3847d.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 04/13] clar: avoid compile error with mingw-w64","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:19Z","receivedAt":"2024-08-20T14:02:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen using mingw-w64 to compile the code, and using `_stat()`, it is\nnecessary to use `struct _stat`, too, and not `struct stat` (as the\nlatter is incompatible with the \"dashed\" version because it is limited\nto 32-bit time types for backwards compatibility).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nindex 3fc2c768158..e2ebe551d38 100644\n--- a/t/unit-tests/clar/clar.c\n+++ b/t/unit-tests/clar/clar.c\n@@ -68,7 +68,7 @@\n #\t\tdefine PRIxZ \"Ix\"\n #\tendif\n \n-#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+#\tif defined(_MSC_VER) || (defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))\n \ttypedef struct stat STAT_T;\n #\telse\n \ttypedef struct _stat STAT_T;\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501344","messageId":"f88b3421a095ba43d9fe2609f45eae4a0d44ad0e.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 05/13] clar(win32): avoid compile error due to unused `fs_copy()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:25Z","receivedAt":"2024-08-20T14:02:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen CLAR_FIXTURE_PATH is unset, the `fs_copy()` function seems not to\nbe used. But it is declared as `static`, and GCC does not like that,\ncomplaining that it should not be declared/defined to begin with.\n\nWe could mark this function as (potentially) unused by following the\n`MAYBE_UNUSED` pattern from Git's `git-compat-util.h`. However, this is\na GCC-only construct that is not understood by Visual C. Besides, `clar`\ndoes not use that pattern at all.\n\nInstead, let's use the `((void)SYMBOL);` pattern that `clar` already\nuses elsewhere; This avoids the compile error by sorta kinda make the\nfunction used after a fashion.\n\nNote: GCC 14.x (which Git for Windows' SDK already uses) is able to\nfigure out that this function is unused even though there are recursive\ncalls between `fs_copy()` and `fs_copydir_helper()`; Earlier GCC\nversions do not detect that, and therefore the issue has been hidden\nfrom the regular Linux CI builds (where GCC 14.x is not yet used). That\nis the reason why this change is only made in the Windows-specific\nportion of `t/unit-tests/clar/clar/fs.h`.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/fs.h | 2 ++\n 1 file changed, 2 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nindex 3e39890bd3e..8b206179fc4 100644\n--- a/t/unit-tests/clar/clar/fs.h\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -297,6 +297,8 @@ cl_fs_cleanup(void)\n {\n #ifdef CLAR_FIXTURE_PATH\n \tfs_rm(fixture_path(_clar_path, \"*\"));\n+#else\n+\t((void)fs_copy); /* unused */\n #endif\n }\n \n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501345","messageId":"5fb4c55be334c084b92fd46a788afa9676cf5ed0.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 06/13] clar: stop including `shellapi.h` unnecessarily","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:27Z","receivedAt":"2024-08-20T14:02:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThe `shellapi.h` header was included as of\nhttps://github.com/clar-test/clar/commit/136e763211aa, to have\n`SHFileOperation()` declared so that it could be called.\n\nHowever, https://github.com/clar-test/clar/commit/5ce31b69b525 removed\nthat call, and therefore that `#include <shellapi.h>` is unnecessary.\n\nIt is also unwanted in Git because this project uses a subset of Git for\nWindows' SDK in its CI builds that (for bandwidth reasons) excludes tons\nof header files, including `shellapi.h`.\n\nSo let's remove it.\n\nNote: Since the `windows.h` header would include `shellapi.h` anyway, we\nalso define `WIN32_LEAN_AND_MEAN` to avoid this and similar other\nunnecessary includes before including `windows.h`.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nindex e2ebe551d38..cef0f023c24 100644\n--- a/t/unit-tests/clar/clar.c\n+++ b/t/unit-tests/clar/clar.c\n@@ -19,9 +19,9 @@\n #include <sys/stat.h>\n \n #ifdef _WIN32\n+#\tdefine WIN32_LEAN_AND_MEAN\n #\tinclude <windows.h>\n #\tinclude <io.h>\n-#\tinclude <shellapi.h>\n #\tinclude <direct.h>\n \n #\tdefine _MAIN_CC __cdecl\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501346","messageId":"e0dcbd5ca833481b2b08ac3355c70ab18e78e77b.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 07/13] Makefile: fix sparse dependency on GENERATED_H","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:30Z","receivedAt":"2024-08-20T14:02:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"check\" Makefile target is essentially an alias around the \"sparse\"\ntarget. The one difference though is that it will tell users to instead\nrun the \"test\" target in case they do not have sparse(1) installed, as\nchances are high that they wanted to execute the test suite rather than\ndoing semantic checks.\n\nBut even though the \"check\" target ultimately just ends up executing\n`make sparse`, it still depends on our generated headers. This does not\nmake any sense though: they are irrelevant for the \"test\" target advice,\nand if these headers are required for the \"sparse\" target they must be\ndeclared as a dependency on the aliased target, not the alias.\n\nBut even moving the dependency to the \"sparse\" target is wrong, as\nconcurrent builds may then end up generating the headers and running\nsparse concurrently. Instead, we make them a dependency of the specific\nobjects. While that is overly broad, it does ensure correct ordering.\nThe alternative, specifying which file depends on what generated header\nexplicitly, feels rather unmaintainable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 28742a60964..efd305ab358 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3254,7 +3254,7 @@ check-sha1:: t/helper/test-tool$X\n \n SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n \n-$(SP_OBJ): %.sp: %.c %.o\n+$(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n \t\t-Wsparse-error \\\n \t\t$(SPARSE_FLAGS) $(SP_EXTRA_FLAGS) $< && \\\n@@ -3295,7 +3295,7 @@ style:\n \tgit clang-format --style file --diff --extensions c,h\n \n .PHONY: check\n-check: $(GENERATED_H)\n+check:\n \t@if sparse; \\\n \tthen \\\n \t\techo >&2 \"Use 'make sparse' instead\"; \\\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501347","messageId":"77a03f8df70387cb54c01dcc8b43beed829ccda8.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 08/13] Makefile: make hdr-check depend on generated headers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:33Z","receivedAt":"2024-08-20T14:02:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"hdr-check\" Makefile target compiles each of our headers as a\nstandalone code unit to ensure that they are not missing any type\ndeclarations and can be included standalone.\n\nWith the next commit we will wire up the clar unit testing framework,\nwhich will have the effect that some headers start depending on\ngenerated ones. While we could declare that dependency explicitly, it\ndoes not really feel very maintainable in the future.\n\nInstead, we do the same as in the preceding commit and have the objects\ndepend on all of our generated headers. While again overly broad, it is\neasy to maintain and generating headers is not an expensive thing to do\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex efd305ab358..8c4487dd0c6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3284,7 +3284,7 @@ HCC = $(HCO:hco=hcc)\n \t@echo '#include \"git-compat-util.h\"' >$@\n \t@echo '#include \"$<\"' >>$@\n \n-$(HCO): %.hco: %.hcc FORCE\n+$(HCO): %.hco: %.hcc $(GENERATED_H) FORCE\n \t$(QUIET_HDR)$(CC) $(ALL_CFLAGS) -o /dev/null -c -xc $<\n \n .PHONY: hdr-check $(HCO)\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501348","messageId":"c91dd7327e36e02f268122c1e85c3c50c74b3b43.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 09/13] Makefile: do not use sparse on third-party sources","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:36Z","receivedAt":"2024-08-20T14:02:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have several third-party sources in our codebase that we have\nimported from upstream projects. These sources are mostly excluded from\nour static analysis, for example when running Coccinelle.\n\nDo the same for our \"sparse\" target by filtering them out.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 8c4487dd0c6..81a47b61327 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3252,7 +3252,8 @@ t/helper/test-%$X: t/helper/test-%.o GIT-LDFLAGS $(GITLIBS) $(REFTABLE_TEST_LIB)\n check-sha1:: t/helper/test-tool$X\n \tt/helper/test-sha1.sh\n \n-SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n+SP_SRC = $(filter-out $(THIRD_PARTY_SOURCES),$(patsubst %.o,%.c,$(OBJECTS)))\n+SP_OBJ = $(patsubst %.c,%.sp,$(SP_SRC))\n \n $(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501349","messageId":"115c15aa9ae909e61d5f07e5e63e5988a4a3bd8a.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 10/13] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:39Z","receivedAt":"2024-08-20T14:02:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Wire up the clar unit testing framework by introducing a new\n\"unit-tests\" executable. In contrast to the existing framework, this\nwill result in a single executable for all test suites. The ability to\npick specific tests to execute is retained via functionality built into\nthe clar itself.\n\nNote that we need to be a bit careful about how we need to invalidate\nour Makefile rules. While we obviously have to regenerate the clar suite\nwhen our test suites change, we also have to invalidate it in case any\nof the test suites gets removed. We do so by using our typical pattern\nof creating a `GIT-TEST-SUITES` file that gets updated whenever the set\nof test suites changes, so that we can easily depend on that file.\n\nAnother specialty is that we generate a \"clar-decls.h\" file. The test\nfunctions are neither static, nor do they have external declarations.\nThis is because they are getting parsed via \"generate.py\", which then\ncreates the external generations that get populated into an array. These\ndeclarations are only seen by the main function though.\n\nThe consequence is that we will get a bunch of \"missing prototypes\"\nerrors from our compiler for each of these test functions. To fix those\nerrors, we extract the `extern` declarations from \"clar.suite\" and put\nthem into a standalone header that then gets included by each of our\nunit tests. This gets rid of compiler warnings for every function which\nhas been extracted by \"generate.py\". More importantly though, it does\n_not_ get rid of warnings in case a function really isn't being used by\nanything. Thus, it would cause a compiler error if a function name was\nmistyped and thus not picked up by \"generate.py\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Makefile                       | 36 +++++++++++++++++++++---\n t/Makefile                     |  1 +\n t/unit-tests/.gitignore        |  2 ++\n t/unit-tests/clar-generate.awk | 50 ++++++++++++++++++++++++++++++++++\n t/unit-tests/unit-test.c       | 18 ++++++++++++\n t/unit-tests/unit-test.h       |  3 ++\n 7 files changed, 107 insertions(+), 4 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\ndiff --git a/.gitignore b/.gitignore\nindex 8caf3700c23..6687bd6db4c 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,6 +9,7 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n+/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex 81a47b61327..e38146b5eb0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -914,6 +914,8 @@ REFTABLE_TEST_LIB = reftable/libreftable_test.a\n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n GENERATED_H += hook-list.h\n+GENERATED_H += $(UNIT_TEST_DIR)/clar-decls.h\n+GENERATED_H += $(UNIT_TEST_DIR)/clar.suite\n \n .PHONY: generated-hdrs\n generated-hdrs: $(GENERATED_H)\n@@ -1334,6 +1336,11 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n+UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n+\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n@@ -2714,6 +2721,7 @@ OBJECTS += $(XDIFF_OBJS)\n OBJECTS += $(FUZZ_OBJS)\n OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n OBJECTS += $(UNIT_TEST_OBJS)\n+OBJECTS += $(UNIT_TESTS_OBJS)\n \n ifndef NO_CURL\n \tOBJECTS += http.o http-walker.o remote-curl.o\n@@ -3216,7 +3224,7 @@ endif\n \n test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))\n \n-all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)\n+all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n \n bin-wrappers/%: wrap-for-bin.sh\n \t$(call mkdir_p_parent_template)\n@@ -3648,7 +3656,7 @@ endif\n \n artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n \t\tGIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \\\n-\t\t$(UNIT_TEST_PROGS) $(MOFILES)\n+\t\t$(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) $(MOFILES)\n \t$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \\\n \t\tSHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'\n \ttest -n \"$(ARTIFACTS_DIRECTORY)\"\n@@ -3704,6 +3712,7 @@ cocciclean:\n \n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n+\t$(RM) GIT-TEST-SUITES\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3863,7 +3872,26 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n \n+GIT-TEST-SUITES: FORCE\n+\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n+\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n+\t\techo >&2 \"    * new test suites\"; \\\n+\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n+            fi\n+\n+$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n+\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n+\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n+\tdone >$@\n+$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n+\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n+$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR)\n+$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n+\t$(call mkdir_p_parent_template)\n+\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n+\n .PHONY: build-unit-tests unit-tests\n-build-unit-tests: $(UNIT_TEST_PROGS)\n-unit-tests: $(UNIT_TEST_PROGS) t/helper/test-tool$X\n+build-unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n+unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) t/helper/test-tool$X\n \t$(MAKE) -C t/ unit-tests\ndiff --git a/t/Makefile b/t/Makefile\nindex d2212de0b78..131ffd778fe 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -48,6 +48,7 @@ CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.tes\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n+UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n UNIT_TESTS = $(sort $(UNIT_TEST_PROGRAMS))\n UNIT_TESTS_NO_DIR = $(notdir $(UNIT_TESTS))\n \ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex 5e56e040ec0..d0632ec7f9e 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1 +1,3 @@\n /bin\n+/clar.suite\n+/clar-decls.h\ndiff --git a/t/unit-tests/clar-generate.awk b/t/unit-tests/clar-generate.awk\nnew file mode 100644\nindex 00000000000..ab71ce6c9fc\n--- /dev/null\n+++ b/t/unit-tests/clar-generate.awk\n@@ -0,0 +1,50 @@\n+function add_suite(suite, initialize, cleanup, count) {\n+       if (!suite) return\n+       suite_count++\n+       callback_count += count\n+       suites = suites \"    {\\n\"\n+       suites = suites \"        \\\"\" suite \"\\\",\\n\"\n+       suites = suites \"        \" initialize \",\\n\"\n+       suites = suites \"        \" cleanup \",\\n\"\n+       suites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n+       suites = suites \"    },\\n\"\n+}\n+\n+BEGIN {\n+       suites = \"static struct clar_suite _clar_suites[] = {\\n\"\n+}\n+\n+{\n+       print\n+       name = $3; sub(/\\(.*$/, \"\", name)\n+       suite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n+       short_name = name; sub(/^.*__/, \"\", short_name)\n+       cb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n+       if (suite != prev_suite) {\n+               add_suite(prev_suite, initialize, cleanup, count)\n+               if (callbacks) callbacks = callbacks \"};\\n\"\n+               callbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n+               initialize = \"{ NULL, NULL }\"\n+               cleanup = \"{ NULL, NULL }\"\n+               count = 0\n+               prev_suite = suite\n+       }\n+       if (short_name == \"initialize\") {\n+               initialize = cb\n+       } else if (short_name == \"cleanup\") {\n+               cleanup = cb\n+       } else {\n+               callbacks = callbacks \"    \" cb \",\\n\"\n+               count++\n+       }\n+}\n+\n+END {\n+       add_suite(suite, initialize, cleanup, count)\n+       suites = suites \"};\"\n+       if (callbacks) callbacks = callbacks \"};\"\n+       print callbacks\n+       print suites\n+       print \"static const size_t _clar_suite_count = \" suite_count \";\"\n+       print \"static const size_t _clar_callback_count = \" callback_count \";\"\n+}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nnew file mode 100644\nindex 00000000000..32a81299e91\n--- /dev/null\n+++ b/t/unit-tests/unit-test.c\n@@ -0,0 +1,18 @@\n+#include \"unit-test.h\"\n+\n+int cmd_main(int argc, const char **argv)\n+{\n+\tconst char **argv_copy;\n+\tint ret;\n+\n+\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n+\tALLOC_ARRAY(argv_copy, argc + 2);\n+\tCOPY_ARRAY(argv_copy, argv, argc);\n+\targv_copy[argc++] = \"-t\";\n+\targv_copy[argc] = NULL;\n+\n+\tret = clar_test(argc, (char **) argv_copy);\n+\n+\tfree(argv_copy);\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nnew file mode 100644\nindex 00000000000..66ec2387cc6\n--- /dev/null\n+++ b/t/unit-tests/unit-test.h\n@@ -0,0 +1,3 @@\n+#include \"git-compat-util.h\"\n+#include \"clar/clar.h\"\n+#include \"clar-decls.h\"\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501350","messageId":"b3b8df048725c25b14860513b7950b158a6990ea.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 11/13] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:43Z","receivedAt":"2024-08-20T14:02:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the strvec tests to use the new clar unit testing framework.\nThis is a first test balloon that demonstrates how the testing infra for\nclar-based tests looks like.\n\nThe tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\nrunning that binary, it generates TAP output:\n\n    # ./t/unit-tests/bin/unit-tests\n    TAP version 13\n    # start of suite 1: strvec\n    ok 1 - strvec::init\n    ok 2 - strvec::dynamic_init\n    ok 3 - strvec::clear\n    ok 4 - strvec::push\n    ok 5 - strvec::pushft_pushf\n    ok 6 - strvec::pushl\n    ok 7 - strvec::pushv\n    ok 8 - strvec::replace_at_head\n    ok 9 - strvec::replace_at_tail\n    ok 10 - strvec::replace_in_between\n    ok 11 - strvec::replace_with_substring\n    ok 12 - strvec::remove_at_head\n    ok 13 - strvec::remove_at_tail\n    ok 14 - strvec::remove_in_between\n    ok 15 - strvec::pop_empty_array\n    ok 16 - strvec::pop_non_empty_array\n    ok 17 - strvec::split_empty_string\n    ok 18 - strvec::split_single_item\n    ok 19 - strvec::split_multiple_items\n    ok 20 - strvec::split_whitespace_only\n    ok 21 - strvec::split_multiple_consecutive_whitespaces\n    ok 22 - strvec::detach\n    1..22\n\nThe binary also supports some parameters that allow us to run only a\nsubset of unit tests or alter the output:\n\n    $ ./t/unit-tests/bin/unit-tests -h\n    Usage: ./t/unit-tests/bin/unit-tests [options]\n\n    Options:\n      -sname        Run only the suite with `name` (can go to individual test name)\n      -iname        Include the suite with `name`\n      -xname        Exclude the suite with `name`\n      -v            Increase verbosity (show suite names)\n      -q            Only report tests that had an error\n      -Q            Quit as soon as a test fails\n      -t            Display results in tap format\n      -l            Print suite names\n      -r[filename]  Write summary file (to the optional filename)\n\nFurthermore, running `make unit-tests` runs the binary along with all\nthe other unit tests we have.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                |   2 +-\n t/unit-tests/strvec.c   | 222 ++++++++++++++++++++++++++++++++++++++++\n t/unit-tests/t-strvec.c | 211 --------------------------------------\n 3 files changed, 223 insertions(+), 212 deletions(-)\n create mode 100644 t/unit-tests/strvec.c\n delete mode 100644 t/unit-tests/t-strvec.c\n\ndiff --git a/Makefile b/Makefile\nindex e38146b5eb0..56ce6c00e44 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n@@ -1356,7 +1357,6 @@ UNIT_TEST_PROGRAMS += t-reftable-record\n UNIT_TEST_PROGRAMS += t-reftable-tree\n UNIT_TEST_PROGRAMS += t-strbuf\n UNIT_TEST_PROGRAMS += t-strcmp-offset\n-UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-trailer\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\ndiff --git a/t/unit-tests/strvec.c b/t/unit-tests/strvec.c\nnew file mode 100644\nindex 00000000000..d11ed0f28de\n--- /dev/null\n+++ b/t/unit-tests/strvec.c\n@@ -0,0 +1,222 @@\n+#include \"unit-test.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+#define check_strvec(vec, ...) \\\n+\tdo { \\\n+\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n+\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n+\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n+\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n+\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n+\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n+\t} while (0)\n+\n+void test_strvec__init(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__dynamic_init(void)\n+{\n+\tstruct strvec vec;\n+\tstrvec_init(&vec);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__clear(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_push(&vec, \"foo\");\n+\tstrvec_clear(&vec);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__push(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\n+\tstrvec_push(&vec, \"bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushf(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushf(&vec, \"foo: %d\", 1);\n+\tcheck_strvec(&vec, \"foo: 1\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushl(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushv(void)\n+{\n+\tconst char *strings[] = {\n+\t\t\"foo\", \"bar\", \"baz\", NULL,\n+\t};\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushv(&vec, strings);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 0, \"replaced\");\n+\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 2, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 1, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_with_substring(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", NULL);\n+\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n+\tcheck_strvec(&vec, \"oo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 0);\n+\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 2);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 1);\n+\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pop_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pop_non_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_empty_string(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_single_item(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_multiple_items(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo bar baz\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_whitespace_only(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \" \\t\\n\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_multiple_consecutive_whitespaces(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_split(&vec, \"foo\\n\\t bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__detach(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tconst char **detached;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\n+\tdetached = strvec_detach(&vec);\n+\tcl_assert_equal_s(detached[0], \"foo\");\n+\tcl_assert_equal_p(detached[1], NULL);\n+\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+\n+\tfree((char *) detached[0]);\n+\tfree(detached);\n+}\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\ndeleted file mode 100644\nindex c4bac8fc91b..00000000000\n--- a/t/unit-tests/t-strvec.c\n+++ /dev/null\n@@ -1,211 +0,0 @@\n-#include \"test-lib.h\"\n-#include \"strbuf.h\"\n-#include \"strvec.h\"\n-\n-#define check_strvec(vec, ...) \\\n-\tdo { \\\n-\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n-\t\t\t\t\t\t (uintmax_t)i); \\\n-\t\t\t\t\tbreak; \\\n-\t\t\t\t} \\\n-\t\t\t} \\\n-\t\t} \\\n-\t} while (0)\n-\n-int cmd_main(int argc, const char **argv)\n-{\n-\tif_test (\"static initialization\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"dynamic initialization\") {\n-\t\tstruct strvec vec;\n-\t\tstrvec_init(&vec);\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"clear\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_push(&vec, \"foo\");\n-\t\tstrvec_clear(&vec);\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"push\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\n-\t\tstrvec_push(&vec, \"foo\");\n-\t\tcheck_strvec(&vec, \"foo\", NULL);\n-\n-\t\tstrvec_push(&vec, \"bar\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushf\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushf(&vec, \"foo: %d\", 1);\n-\t\tcheck_strvec(&vec, \"foo: 1\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushl\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushv\") {\n-\t\tconst char *strings[] = {\n-\t\t\t\"foo\", \"bar\", \"baz\", NULL,\n-\t\t};\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\n-\t\tstrvec_pushv(&vec, strings);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace at head\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 0, \"replaced\");\n-\t\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace at tail\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 2, \"replaced\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace in between\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 1, \"replaced\");\n-\t\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace with substring\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", NULL);\n-\t\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n-\t\tcheck_strvec(&vec, \"oo\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove at head\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 0);\n-\t\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove at tail\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 2);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove in between\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 1);\n-\t\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pop with empty array\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pop(&vec);\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pop with non-empty array\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_pop(&vec);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split empty string\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"\");\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split single item\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo\");\n-\t\tcheck_strvec(&vec, \"foo\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split multiple items\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo bar baz\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split whitespace only\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \" \\t\\n\");\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split multiple consecutive whitespaces\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo\\n\\t bar\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"detach\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tconst char **detached;\n-\n-\t\tstrvec_push(&vec, \"foo\");\n-\n-\t\tdetached = strvec_detach(&vec);\n-\t\tcheck_str(detached[0], \"foo\");\n-\t\tcheck_pointer_eq(detached[1], NULL);\n-\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\n-\t\tfree((char *) detached[0]);\n-\t\tfree(detached);\n-\t}\n-\n-\treturn test_done();\n-}\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501351","messageId":"1ac2e48a7f2d41d60ff56890d8d87125f30c2f76.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 12/13] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:46Z","receivedAt":"2024-08-20T14:02:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the ctype tests to use the new clar unit testing framework.\nIntroduce a new function `cl_failf()` that allows us to print a\nformatted error message, which we can use to point out which of the\ncharacters was classified incorrectly. This results in output like this\non failure:\n\n    # start of suite 1: ctype\n    ok 1 - ctype::isspace\n    not ok 2 - ctype::isdigit\n        ---\n        reason: |\n          Test failed.\n          0x61 is classified incorrectly\n        at:\n          file: 't/unit-tests/ctype.c'\n          line: 38\n          function: 'test_ctype__isdigit'\n        ---\n    ok 3 - ctype::isalpha\n    ok 4 - ctype::isalnum\n    ok 5 - ctype::is_glob_special\n    ok 6 - ctype::is_regex_special\n    ok 7 - ctype::is_pathspec_magic\n    ok 8 - ctype::isascii\n    ok 9 - ctype::islower\n    ok 10 - ctype::isupper\n    ok 11 - ctype::iscntrl\n    ok 12 - ctype::ispunct\n    ok 13 - ctype::isxdigit\n    ok 14 - ctype::isprint\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                            |  2 +-\n t/unit-tests/{t-ctype.c => ctype.c} | 70 ++++++++++++++++++++++++-----\n t/unit-tests/unit-test.h            |  7 +++\n 3 files changed, 67 insertions(+), 12 deletions(-)\n rename t/unit-tests/{t-ctype.c => ctype.c} (70%)\n\ndiff --git a/Makefile b/Makefile\nindex 56ce6c00e44..c841cf70063 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,13 +1336,13 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += ctype\n UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n \n-UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n UNIT_TEST_PROGRAMS += t-hashmap\ndiff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/ctype.c\nsimilarity index 70%\nrename from t/unit-tests/t-ctype.c\nrename to t/unit-tests/ctype.c\nindex e28a7f50f9a..5026378bfbc 100644\n--- a/t/unit-tests/t-ctype.c\n+++ b/t/unit-tests/ctype.c\n@@ -1,16 +1,13 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n \n #define TEST_CHAR_CLASS(class, string) do { \\\n \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n-\tif_test (#class \" works\") { \\\n-\t\tfor (int i = 0; i < 256; i++) { \\\n-\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n-\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n-\t\t} \\\n-\t\tcheck(!class(EOF)); \\\n-\t} \\\n+\tfor (int i = 0; i < 256; i++) \\\n+\t\tif (class(i) != !!memchr(string, i, len)) \\\n+\t\t\tcl_failf(\"0x%02x is classified incorrectly\", i); \\\n+\tcl_assert(!class(EOF)); \\\n } while (0)\n \n #define DIGIT \"0123456789\"\n@@ -31,21 +28,72 @@\n \t\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\" \\\n \t\"\\x7f\"\n \n-int cmd_main(int argc, const char **argv) {\n+void test_ctype__isspace(void)\n+{\n \tTEST_CHAR_CLASS(isspace, \" \\n\\r\\t\");\n+}\n+\n+void test_ctype__isdigit(void)\n+{\n \tTEST_CHAR_CLASS(isdigit, DIGIT);\n+}\n+\n+void test_ctype__isalpha(void)\n+{\n \tTEST_CHAR_CLASS(isalpha, LOWER UPPER);\n+}\n+\n+void test_ctype__isalnum(void)\n+{\n \tTEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);\n+}\n+\n+void test_ctype__is_glob_special(void)\n+{\n \tTEST_CHAR_CLASS(is_glob_special, \"*?[\\\\\");\n+}\n+\n+void test_ctype__is_regex_special(void)\n+{\n \tTEST_CHAR_CLASS(is_regex_special, \"$()*+.?[\\\\^{|\");\n+}\n+\n+void test_ctype__is_pathspec_magic(void)\n+{\n \tTEST_CHAR_CLASS(is_pathspec_magic, \"!\\\"#%&',-/:;<=>@_`~\");\n+}\n+\n+void test_ctype__isascii(void)\n+{\n \tTEST_CHAR_CLASS(isascii, ASCII);\n+}\n+\n+void test_ctype__islower(void)\n+{\n \tTEST_CHAR_CLASS(islower, LOWER);\n+}\n+\n+void test_ctype__isupper(void)\n+{\n \tTEST_CHAR_CLASS(isupper, UPPER);\n+}\n+\n+void test_ctype__iscntrl(void)\n+{\n \tTEST_CHAR_CLASS(iscntrl, CNTRL);\n+}\n+\n+void test_ctype__ispunct(void)\n+{\n \tTEST_CHAR_CLASS(ispunct, PUNCT);\n+}\n+\n+void test_ctype__isxdigit(void)\n+{\n \tTEST_CHAR_CLASS(isxdigit, DIGIT \"abcdefABCDEF\");\n-\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n+}\n \n-\treturn test_done();\n+void test_ctype__isprint(void)\n+{\n+\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n }\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nindex 66ec2387cc6..4c461defe16 100644\n--- a/t/unit-tests/unit-test.h\n+++ b/t/unit-tests/unit-test.h\n@@ -1,3 +1,10 @@\n #include \"git-compat-util.h\"\n #include \"clar/clar.h\"\n #include \"clar-decls.h\"\n+#include \"strbuf.h\"\n+\n+#define cl_failf(fmt, ...) do { \\\n+\tchar *desc = xstrfmt(fmt, __VA_ARGS__); \\\n+\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n+\tfree(desc); \\\n+} while (0)\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501352","messageId":"131036c398eda859255ecf4a6162be90ca1db1c3.1724159966.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"[PATCH v6 13/13] clar: add CMake support","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-20T14:02:49Z","receivedAt":"2024-08-20T14:02:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nNow that we're using `clar` as powerful test framework, we have to\nadjust the Visual C build (read: the CMake definition) to be able to\nhandle that, too.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n contrib/buildsystems/CMakeLists.txt | 53 +++++++++++++++++++++++++++++\n 1 file changed, 53 insertions(+)\n\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 832f46b316b..608fd3fe709 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -1004,6 +1004,59 @@ foreach(unit_test ${unit_test_PROGRAMS})\n \tendif()\n endforeach()\n \n+parse_makefile_for_scripts(unit_tests_SUITES \"UNIT_TESTS_SUITES\" \"\")\n+\n+set(clar_decls \"\")\n+set(clar_cbs \"\")\n+set(clar_cbs_count 0)\n+set(clar_suites \"static struct clar_suite _clar_suites[] = {\\n\")\n+list(LENGTH unit_tests_SUITES clar_suites_count)\n+foreach(suite ${unit_tests_SUITES})\n+\tfile(STRINGS \"${CMAKE_SOURCE_DIR}/t/unit-tests/${suite}.c\" decls\n+\t\tREGEX \"^void test_${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*\\\\(void\\\\)$\")\n+\n+\tlist(LENGTH decls decls_count)\n+\tstring(REGEX REPLACE \"void (test_${suite}__([a-zA-Z_0-9]*))\\\\(void\\\\)\" \"    { \\\"\\\\2\\\", &\\\\1 },\\n\" cbs ${decls})\n+\tstring(JOIN \"\" cbs ${cbs})\n+\tlist(TRANSFORM decls PREPEND \"extern \")\n+\tstring(JOIN \";\\n\" decls ${decls})\n+\n+\tstring(APPEND clar_decls \"${decls};\\n\")\n+\tstring(APPEND clar_cbs\n+\t\t\"static const struct clar_func _clar_cb_${suite}[] = {\\n\"\n+\t\t${cbs}\n+\t\t\"};\\n\")\n+\tstring(APPEND clar_suites\n+\t\t\"    {\\n\"\n+\t\t\"        \\\"${suite}\\\",\\n\"\n+\t\t\"        { NULL, NULL },\\n\"\n+\t\t\"        { NULL, NULL },\\n\"\n+\t\t\"        _clar_cb_${suite}, ${decls_count}, 1\\n\"\n+\t\t\"    },\\n\")\n+\tmath(EXPR clar_cbs_count \"${clar_cbs_count}+${decls_count}\")\n+endforeach()\n+string(APPEND clar_suites\n+\t\"};\\n\"\n+\t\"static const size_t _clar_suite_count = ${clar_suites_count};\\n\"\n+\t\"static const size_t _clar_callback_count = ${clar_cbs_count};\\n\")\n+file(WRITE \"${CMAKE_BINARY_DIR}/t/unit-tests/clar-decls.h\" \"${clar_decls}\")\n+file(WRITE \"${CMAKE_BINARY_DIR}/t/unit-tests/clar.suite\" \"${clar_decls}\" \"${clar_cbs}\" \"${clar_suites}\")\n+\n+list(TRANSFORM unit_tests_SUITES PREPEND \"${CMAKE_SOURCE_DIR}/t/unit-tests/\")\n+list(TRANSFORM unit_tests_SUITES APPEND \".c\")\n+add_library(unit-tests-lib ${unit_tests_SUITES} \"${CMAKE_SOURCE_DIR}/t/unit-tests/clar/clar.c\")\n+target_include_directories(unit-tests-lib PRIVATE \"${CMAKE_SOURCE_DIR}/t/unit-tests\")\n+add_executable(unit-tests \"${CMAKE_SOURCE_DIR}/t/unit-tests/unit-test.c\")\n+target_link_libraries(unit-tests unit-tests-lib common-main)\n+set_target_properties(unit-tests\n+\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+if(MSVC)\n+\tset_target_properties(unit-tests\n+\t\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+\tset_target_properties(unit-tests\n+\t\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+endif()\n+\n #test-tool\n parse_makefile_for_sources(test-tool_SOURCES \"TEST_BUILTINS_OBJS\")\n add_library(test-lib OBJECT ${CMAKE_SOURCE_DIR}/t/unit-tests/test-lib.c)\n-- \n2.46.0.164.g477ce5ccd6.dirty\n\n"},{"id":"501589","messageId":"845fa867-f339-9190-b9fa-2e81c02adb6b@gmx.de","threadId":"61874","inReplyTo":"831543c6-2282-44f7-bf05-b66c2d6b08cc@gmail.com","subject":"Re: [PATCH v5 2/9] t: import the clar unit testing framework","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-08-23T12:16:07Z","receivedAt":"2024-08-23T12:16:39Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Phillip,\n\nthank you for looping me in; I meant to answer earlier.\n\nI am very, very happy to see that there is a chance to adopt a test\nframework like `clar`. Therefore, I am very interested in assisting in\nevery which way I can.\n\nOn Fri, 16 Aug 2024, Phillip Wood wrote:\n\n> Johannes - are these format specifiers supported by the version of MSVCRT that\n> is used by git for windows? c.f.\n> https://lore.kernel.org/git/nycvar.QRO.7.76.6.2203241456250.388@tvgsbejvaqbjf.bet/\n\nThe format specifiers _should_ work well both with GCC and Visual C. What\ndid not work well was using `struct stat` with `_stat()` (note that the\nlatter has a leading underscore and the former does not).\n\nThere were other issues, top of which was the bit tricky part to adapt the\n`CMakeLists.txt` definition, in particular the part where `clar.suite` is\ngenerated. But I managed to squeeze in that work somehow and Patrick was\nkind enough to already integrate the patches.\n\nCiao,\nJohannes\n\n> On 16/08/2024 08:04, Patrick Steinhardt wrote:\n> > diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\n> > [...]\n> > +#ifdef _WIN32\n> > [...]\n> > +#\tifndef PRIuZ\n> > +#\t\tdefine PRIuZ \"Iu\"\n> > +#\tendif\n> > +#\tifndef PRIxZ\n> > +#\t\tdefine PRIxZ \"Ix\"\n> > +#\tendif\n>\n> Thanks\n>\n> Phillip\n>\n> > +#\tif defined(_MSC_VER) || defined(__MINGW32__)\n> > +\ttypedef struct stat STAT_T;\n> > +#\telse\n> > +\ttypedef struct _stat STAT_T;\n> > +#\tendif\n> > +#else\n> > +#\tinclude <sys/wait.h> /* waitpid(2) */\n> > +#\tinclude <unistd.h>\n> > +#\tdefine _MAIN_CC\n> > +#\tdefine p_snprintf snprintf\n> > +#\tifndef PRIuZ\n> > +#\t\tdefine PRIuZ \"zu\"\n> > +#\tendif\n> > +#\tifndef PRIxZ\n> > +#\t\tdefine PRIxZ \"zx\"\n> > +#\tendif\n> > +\ttypedef struct stat STAT_T;\n> > +#endif\n> > +\n> > +#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n> > +\n> > +#include \"clar.h\"\n> > +\n> > +static void fs_rm(const char *_source);\n> > +static void fs_copy(const char *_source, const char *dest);\n> > +\n> > +#ifdef CLAR_FIXTURE_PATH\n> > +static const char *\n> > +fixture_path(const char *base, const char *fixture_name);\n> > +#endif\n> > +\n> > +struct clar_error {\n> > +\tconst char *file;\n> > +\tconst char *function;\n> > +\tsize_t line_number;\n> > +\tconst char *error_msg;\n> > +\tchar *description;\n> > +\n> > +\tstruct clar_error *next;\n> > +};\n> > +\n> > +struct clar_explicit {\n> > +\tsize_t suite_idx;\n> > +\tconst char *filter;\n> > +\n> > +\tstruct clar_explicit *next;\n> > +};\n> > +\n> > +struct clar_report {\n> > +\tconst char *test;\n> > +\tint test_number;\n> > +\tconst char *suite;\n> > +\n> > +\tenum cl_test_status status;\n> > +\ttime_t start;\n> > +\tdouble elapsed;\n> > +\n> > +\tstruct clar_error *errors;\n> > +\tstruct clar_error *last_error;\n> > +\n> > +\tstruct clar_report *next;\n> > +};\n> > +\n> > +struct clar_summary {\n> > +\tconst char *filename;\n> > +\tFILE *fp;\n> > +};\n> > +\n> > +static struct {\n> > +\tenum cl_test_status test_status;\n> > +\n> > +\tconst char *active_test;\n> > +\tconst char *active_suite;\n> > +\n> > +\tint total_skipped;\n> > +\tint total_errors;\n> > +\n> > +\tint tests_ran;\n> > +\tint suites_ran;\n> > +\n> > +\tenum cl_output_format output_format;\n> > +\n> > +\tint report_errors_only;\n> > +\tint exit_on_error;\n> > +\tint verbosity;\n> > +\n> > +\tint write_summary;\n> > +\tchar *summary_filename;\n> > +\tstruct clar_summary *summary;\n> > +\n> > +\tstruct clar_explicit *explicit;\n> > +\tstruct clar_explicit *last_explicit;\n> > +\n> > +\tstruct clar_report *reports;\n> > +\tstruct clar_report *last_report;\n> > +\n> > +\tvoid (*local_cleanup)(void *);\n> > +\tvoid *local_cleanup_payload;\n> > +\n> > +\tjmp_buf trampoline;\n> > +\tint trampoline_enabled;\n> > +\n> > +\tcl_trace_cb *pfn_trace_cb;\n> > +\tvoid *trace_payload;\n> > +\n> > +} _clar;\n> > +\n> > +struct clar_func {\n> > +\tconst char *name;\n> > +\tvoid (*ptr)(void);\n> > +};\n> > +\n> > +struct clar_suite {\n> > +\tconst char *name;\n> > +\tstruct clar_func initialize;\n> > +\tstruct clar_func cleanup;\n> > +\tconst struct clar_func *tests;\n> > +\tsize_t test_count;\n> > +\tint enabled;\n> > +};\n> > +\n> > +/* From clar_print_*.c */\n> > +static void clar_print_init(int test_count, int suite_count, const char\n> > *suite_names);\n> > +static void clar_print_shutdown(int test_count, int suite_count, int\n> > error_count);\n> > +static void clar_print_error(int num, const struct clar_report *report,\n> > const struct clar_error *error);\n> > +static void clar_print_ontest(const char *suite_name, const char\n> > *test_name, int test_number, enum cl_test_status failed);\n> > +static void clar_print_onsuite(const char *suite_name, int suite_index);\n> > +static void clar_print_onabort(const char *msg, ...);\n> > +\n> > +/* From clar_sandbox.c */\n> > +static void clar_unsandbox(void);\n> > +static int clar_sandbox(void);\n> > +\n> > +/* From summary.h */\n> > +static struct clar_summary *clar_summary_init(const char *filename);\n> > +static int clar_summary_shutdown(struct clar_summary *fp);\n> > +\n> > +/* Load the declarations for the test suite */\n> > +#include \"clar.suite\"\n> > +\n> > +\n> > +#define CL_TRACE(ev)\n> > \\\n> > +\tdo {\n> > \\\n> > +\t\tif (_clar.pfn_trace_cb)\n> > \\\n> > +\t\t\t_clar.pfn_trace_cb(ev,\n> > \\\n> > +\t\t\t\t\t\t\t   _clar.active_suite,\n> > \\\n> > +\t\t\t\t\t\t\t   _clar.active_test,\n> > \\\n> > +\n> > _clar.trace_payload);\t\t\t\t\t\\\n> > +\t} while (0)\n> > +\n> > +void cl_trace_register(cl_trace_cb *cb, void *payload)\n> > +{\n> > +\t_clar.pfn_trace_cb = cb;\n> > +\t_clar.trace_payload = payload;\n> > +}\n> > +\n> > +\n> > +/* Core test functions */\n> > +static void\n> > +clar_report_errors(struct clar_report *report)\n> > +{\n> > +\tstruct clar_error *error;\n> > +\tint i = 1;\n> > +\n> > +\tfor (error = report->errors; error; error = error->next)\n> > +\t\tclar_print_error(i++, _clar.last_report, error);\n> > +}\n> > +\n> > +static void\n> > +clar_report_all(void)\n> > +{\n> > +\tstruct clar_report *report;\n> > +\tstruct clar_error *error;\n> > +\tint i = 1;\n> > +\n> > +\tfor (report = _clar.reports; report; report = report->next) {\n> > +\t\tif (report->status != CL_TEST_FAILURE)\n> > +\t\t\tcontinue;\n> > +\n> > +\t\tfor (error = report->errors; error; error = error->next)\n> > +\t\t\tclar_print_error(i++, report, error);\n> > +\t}\n> > +}\n> > +\n> > +#ifdef WIN32\n> > +# define clar_time DWORD\n> > +\n> > +static void clar_time_now(clar_time *out)\n> > +{\n> > +\t*out = GetTickCount();\n> > +}\n> > +\n> > +static double clar_time_diff(clar_time *start, clar_time *end)\n> > +{\n> > +\treturn ((double)*end - (double)*start) / 1000;\n> > +}\n> > +#else\n> > +# include <sys/time.h>\n> > +\n> > +# define clar_time struct timeval\n> > +\n> > +static void clar_time_now(clar_time *out)\n> > +{\n> > +\tstruct timezone tz;\n> > +\n> > +\tgettimeofday(out, &tz);\n> > +}\n> > +\n> > +static double clar_time_diff(clar_time *start, clar_time *end)\n> > +{\n> > +\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n> > +\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n> > +}\n> > +#endif\n> > +\n> > +static void\n> > +clar_run_test(\n> > +\tconst struct clar_suite *suite,\n> > +\tconst struct clar_func *test,\n> > +\tconst struct clar_func *initialize,\n> > +\tconst struct clar_func *cleanup)\n> > +{\n> > +\tclar_time start, end;\n> > +\n> > +\t_clar.trampoline_enabled = 1;\n> > +\n> > +\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n> > +\n> > +\t_clar.last_report->start = time(NULL);\n> > +\tclar_time_now(&start);\n> > +\n> > +\tif (setjmp(_clar.trampoline) == 0) {\n> > +\t\tif (initialize->ptr != NULL)\n> > +\t\t\tinitialize->ptr();\n> > +\n> > +\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n> > +\t\ttest->ptr();\n> > +\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n> > +\t}\n> > +\n> > +\tclar_time_now(&end);\n> > +\n> > +\t_clar.trampoline_enabled = 0;\n> > +\n> > +\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n> > +\t\t_clar.last_report->status = CL_TEST_OK;\n> > +\n> > +\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n> > +\n> > +\tif (_clar.local_cleanup != NULL)\n> > +\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n> > +\n> > +\tif (cleanup->ptr != NULL)\n> > +\t\tcleanup->ptr();\n> > +\n> > +\tCL_TRACE(CL_TRACE__TEST__END);\n> > +\n> > +\t_clar.tests_ran++;\n> > +\n> > +\t/* remove any local-set cleanup methods */\n> > +\t_clar.local_cleanup = NULL;\n> > +\t_clar.local_cleanup_payload = NULL;\n> > +\n> > +\tif (_clar.report_errors_only) {\n> > +\t\tclar_report_errors(_clar.last_report);\n> > +\t} else {\n> > +\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran,\n> > _clar.last_report->status);\n> > +\t}\n> > +}\n> > +\n> > +static void\n> > +clar_run_suite(const struct clar_suite *suite, const char *filter)\n> > +{\n> > +\tconst struct clar_func *test = suite->tests;\n> > +\tsize_t i, matchlen;\n> > +\tstruct clar_report *report;\n> > +\tint exact = 0;\n> > +\n> > +\tif (!suite->enabled)\n> > +\t\treturn;\n> > +\n> > +\tif (_clar.exit_on_error && _clar.total_errors)\n> > +\t\treturn;\n> > +\n> > +\tif (!_clar.report_errors_only)\n> > +\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n> > +\n> > +\t_clar.active_suite = suite->name;\n> > +\t_clar.active_test = NULL;\n> > +\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n> > +\n> > +\tif (filter) {\n> > +\t\tsize_t suitelen = strlen(suite->name);\n> > +\t\tmatchlen = strlen(filter);\n> > +\t\tif (matchlen <= suitelen) {\n> > +\t\t\tfilter = NULL;\n> > +\t\t} else {\n> > +\t\t\tfilter += suitelen;\n> > +\t\t\twhile (*filter == ':')\n> > +\t\t\t\t++filter;\n> > +\t\t\tmatchlen = strlen(filter);\n> > +\n> > +\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n> > +\t\t\t\texact = 1;\n> > +\t\t\t\tmatchlen--;\n> > +\t\t\t}\n> > +\t\t}\n> > +\t}\n> > +\n> > +\tfor (i = 0; i < suite->test_count; ++i) {\n> > +\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n> > +\t\t\tcontinue;\n> > +\n> > +\t\tif (exact && strlen(test[i].name) != matchlen)\n> > +\t\t\tcontinue;\n> > +\n> > +\t\t_clar.active_test = test[i].name;\n> > +\n> > +\t\treport = calloc(1, sizeof(struct clar_report));\n> > +\t\treport->suite = _clar.active_suite;\n> > +\t\treport->test = _clar.active_test;\n> > +\t\treport->test_number = _clar.tests_ran;\n> > +\t\treport->status = CL_TEST_NOTRUN;\n> > +\n> > +\t\tif (_clar.reports == NULL)\n> > +\t\t\t_clar.reports = report;\n> > +\n> > +\t\tif (_clar.last_report != NULL)\n> > +\t\t\t_clar.last_report->next = report;\n> > +\n> > +\t\t_clar.last_report = report;\n> > +\n> > +\t\tclar_run_test(suite, &test[i], &suite->initialize,\n> > &suite->cleanup);\n> > +\n> > +\t\tif (_clar.exit_on_error && _clar.total_errors)\n> > +\t\t\treturn;\n> > +\t}\n> > +\n> > +\t_clar.active_test = NULL;\n> > +\tCL_TRACE(CL_TRACE__SUITE_END);\n> > +}\n> > +\n> > +static void\n> > +clar_usage(const char *arg)\n> > +{\n> > +\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n> > +\tprintf(\"Options:\\n\");\n> > +\tprintf(\"  -sname        Run only the suite with `name` (can go to\n> > individual test name)\\n\");\n> > +\tprintf(\"  -iname        Include the suite with `name`\\n\");\n> > +\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n> > +\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n> > +\tprintf(\"  -q            Only report tests that had an error\\n\");\n> > +\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n> > +\tprintf(\"  -t            Display results in tap format\\n\");\n> > +\tprintf(\"  -l            Print suite names\\n\");\n> > +\tprintf(\"  -r[filename]  Write summary file (to the optional\n> > filename)\\n\");\n> > +\texit(-1);\n> > +}\n> > +\n> > +static void\n> > +clar_parse_args(int argc, char **argv)\n> > +{\n> > +\tint i;\n> > +\n> > +\t/* Verify options before execute */\n> > +\tfor (i = 1; i < argc; ++i) {\n> > +\t\tchar *argument = argv[i];\n> > +\n> > +\t\tif (argument[0] != '-' || argument[1] == '\\0'\n> > +\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n> > +\t\t\tclar_usage(argv[0]);\n> > +\t\t}\n> > +\t}\n> > +\n> > +\tfor (i = 1; i < argc; ++i) {\n> > +\t\tchar *argument = argv[i];\n> > +\n> > +\t\tswitch (argument[1]) {\n> > +\t\tcase 's':\n> > +\t\tcase 'i':\n> > +\t\tcase 'x': { /* given suite name */\n> > +\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n> > +\t\t\tchar action = argument[1];\n> > +\t\t\tsize_t j, arglen, suitelen, cmplen;\n> > +\n> > +\t\t\targument += offset;\n> > +\t\t\targlen = strlen(argument);\n> > +\n> > +\t\t\tif (arglen == 0)\n> > +\t\t\t\tclar_usage(argv[0]);\n> > +\n> > +\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n> > +\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n> > +\t\t\t\tcmplen = (arglen < suitelen) ? arglen :\n> > suitelen;\n> > +\n> > +\t\t\t\tif (strncmp(argument, _clar_suites[j].name,\n> > cmplen) == 0) {\n> > +\t\t\t\t\tint exact = (arglen >= suitelen);\n> > +\n> > +\t\t\t\t\t/* Do we have a real suite prefix\n> > separated by a\n> > +\t\t\t\t\t * trailing '::' or just a matching\n> > substring? */\n> > +\t\t\t\t\tif (arglen > suitelen &&\n> > (argument[suitelen] != ':'\n> > +\t\t\t\t\t\t    || argument[suitelen + 1]\n> > != ':'))\n> > +\t\t\t\t\t    continue;\n> > +\n> > +\t\t\t\t\t++found;\n> > +\n> > +\t\t\t\t\tif (!exact)\n> > +\t\t\t\t\t\t_clar.verbosity =\n> > MAX(_clar.verbosity, 1);\n> > +\n> > +\t\t\t\t\tswitch (action) {\n> > +\t\t\t\t\tcase 's': {\n> > +\t\t\t\t\t\tstruct clar_explicit *explicit\n> > =\n> > +\t\t\t\t\t\t\tcalloc(1,\n> > sizeof(struct clar_explicit));\n> > +\t\t\t\t\t\tassert(explicit);\n> > +\n> > +\t\t\t\t\t\texplicit->suite_idx = j;\n> > +\t\t\t\t\t\texplicit->filter = argument;\n> > +\n> > +\t\t\t\t\t\tif (_clar.explicit == NULL)\n> > +\t\t\t\t\t\t\t_clar.explicit =\n> > explicit;\n> > +\n> > +\t\t\t\t\t\tif (_clar.last_explicit !=\n> > NULL)\n> > +\n> > _clar.last_explicit->next = explicit;\n> > +\n> > +\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n> > +\t\t\t\t\t\t_clar.last_explicit =\n> > explicit;\n> > +\t\t\t\t\t\tbreak;\n> > +\t\t\t\t\t}\n> > +\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1;\n> > break;\n> > +\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0;\n> > break;\n> > +\t\t\t\t\t}\n> > +\n> > +\t\t\t\t\tif (exact)\n> > +\t\t\t\t\t\tbreak;\n> > +\t\t\t\t}\n> > +\t\t\t}\n> > +\n> > +\t\t\tif (!found) {\n> > +\t\t\t\tclar_print_onabort(\"No suite matching '%s'\n> > found.\\n\", argument);\n> > +\t\t\t\texit(-1);\n> > +\t\t\t}\n> > +\t\t\tbreak;\n> > +\t\t}\n> > +\n> > +\t\tcase 'q':\n> > +\t\t\t_clar.report_errors_only = 1;\n> > +\t\t\tbreak;\n> > +\n> > +\t\tcase 'Q':\n> > +\t\t\t_clar.exit_on_error = 1;\n> > +\t\t\tbreak;\n> > +\n> > +\t\tcase 't':\n> > +\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n> > +\t\t\tbreak;\n> > +\n> > +\t\tcase 'l': {\n> > +\t\t\tsize_t j;\n> > +\t\t\tprintf(\"Test suites (use -s<name> to run just\n> > one):\\n\");\n> > +\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n> > +\t\t\t\tprintf(\" %3d: %s\\n\", (int)j,\n> > _clar_suites[j].name);\n> > +\n> > +\t\t\texit(0);\n> > +\t\t}\n> > +\n> > +\t\tcase 'v':\n> > +\t\t\t_clar.verbosity++;\n> > +\t\t\tbreak;\n> > +\n> > +\t\tcase 'r':\n> > +\t\t\t_clar.write_summary = 1;\n> > +\t\t\tfree(_clar.summary_filename);\n> > +\t\t\t_clar.summary_filename = *(argument + 2) ?\n> > strdup(argument + 2) : NULL;\n> > +\t\t\tbreak;\n> > +\n> > +\t\tdefault:\n> > +\t\t\tassert(!\"Unexpected commandline argument!\");\n> > +\t\t}\n> > +\t}\n> > +}\n> > +\n> > +void\n> > +clar_test_init(int argc, char **argv)\n> > +{\n> > +\tconst char *summary_env;\n> > +\n> > +\tif (argc > 1)\n> > +\t\tclar_parse_args(argc, argv);\n> > +\n> > +\tclar_print_init(\n> > +\t\t(int)_clar_callback_count,\n> > +\t\t(int)_clar_suite_count,\n> > +\t\t\"\"\n> > +\t);\n> > +\n> > +\tif (!_clar.summary_filename &&\n> > +\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n> > +\t\t_clar.write_summary = 1;\n> > +\t\t_clar.summary_filename = strdup(summary_env);\n> > +\t}\n> > +\n> > +\tif (_clar.write_summary && !_clar.summary_filename)\n> > +\t\t_clar.summary_filename = strdup(\"summary.xml\");\n> > +\n> > +\tif (_clar.write_summary &&\n> > +\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n> > +\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n> > +\t\texit(-1);\n> > +\t}\n> > +\n> > +\tif (clar_sandbox() < 0) {\n> > +\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n> > +\t\texit(-1);\n> > +\t}\n> > +}\n> > +\n> > +int\n> > +clar_test_run(void)\n> > +{\n> > +\tsize_t i;\n> > +\tstruct clar_explicit *explicit;\n> > +\n> > +\tif (_clar.explicit) {\n> > +\t\tfor (explicit = _clar.explicit; explicit; explicit =\n> > explicit->next)\n> > +\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx],\n> > explicit->filter);\n> > +\t} else {\n> > +\t\tfor (i = 0; i < _clar_suite_count; ++i)\n> > +\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n> > +\t}\n> > +\n> > +\treturn _clar.total_errors;\n> > +}\n> > +\n> > +void\n> > +clar_test_shutdown(void)\n> > +{\n> > +\tstruct clar_explicit *explicit, *explicit_next;\n> > +\tstruct clar_report *report, *report_next;\n> > +\n> > +\tclar_print_shutdown(\n> > +\t\t_clar.tests_ran,\n> > +\t\t(int)_clar_suite_count,\n> > +\t\t_clar.total_errors\n> > +\t);\n> > +\n> > +\tclar_unsandbox();\n> > +\n> > +\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n> > +\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n> > +\t\texit(-1);\n> > +\t}\n> > +\n> > +\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n> > +\t\texplicit_next = explicit->next;\n> > +\t\tfree(explicit);\n> > +\t}\n> > +\n> > +\tfor (report = _clar.reports; report; report = report_next) {\n> > +\t\treport_next = report->next;\n> > +\t\tfree(report);\n> > +\t}\n> > +\n> > +\tfree(_clar.summary_filename);\n> > +}\n> > +\n> > +int\n> > +clar_test(int argc, char **argv)\n> > +{\n> > +\tint errors;\n> > +\n> > +\tclar_test_init(argc, argv);\n> > +\terrors = clar_test_run();\n> > +\tclar_test_shutdown();\n> > +\n> > +\treturn errors;\n> > +}\n> > +\n> > +static void abort_test(void)\n> > +{\n> > +\tif (!_clar.trampoline_enabled) {\n> > +\t\tclar_print_onabort(\n> > +\t\t\t\t\"Fatal error: a cleanup method raised an\n> > exception.\");\n> > +\t\tclar_report_errors(_clar.last_report);\n> > +\t\texit(-1);\n> > +\t}\n> > +\n> > +\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n> > +\tlongjmp(_clar.trampoline, -1);\n> > +}\n> > +\n> > +void clar__skip(void)\n> > +{\n> > +\t_clar.last_report->status = CL_TEST_SKIP;\n> > +\t_clar.total_skipped++;\n> > +\tabort_test();\n> > +}\n> > +\n> > +void clar__fail(\n> > +\tconst char *file,\n> > +\tconst char *function,\n> > +\tsize_t line,\n> > +\tconst char *error_msg,\n> > +\tconst char *description,\n> > +\tint should_abort)\n> > +{\n> > +\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n> > +\n> > +\tif (_clar.last_report->errors == NULL)\n> > +\t\t_clar.last_report->errors = error;\n> > +\n> > +\tif (_clar.last_report->last_error != NULL)\n> > +\t\t_clar.last_report->last_error->next = error;\n> > +\n> > +\t_clar.last_report->last_error = error;\n> > +\n> > +\terror->file = file;\n> > +\terror->function = function;\n> > +\terror->line_number = line;\n> > +\terror->error_msg = error_msg;\n> > +\n> > +\tif (description != NULL)\n> > +\t\terror->description = strdup(description);\n> > +\n> > +\t_clar.total_errors++;\n> > +\t_clar.last_report->status = CL_TEST_FAILURE;\n> > +\n> > +\tif (should_abort)\n> > +\t\tabort_test();\n> > +}\n> > +\n> > +void clar__assert(\n> > +\tint condition,\n> > +\tconst char *file,\n> > +\tconst char *function,\n> > +\tsize_t line,\n> > +\tconst char *error_msg,\n> > +\tconst char *description,\n> > +\tint should_abort)\n> > +{\n> > +\tif (condition)\n> > +\t\treturn;\n> > +\n> > +\tclar__fail(file, function, line, error_msg, description,\n> > should_abort);\n> > +}\n> > +\n> > +void clar__assert_equal(\n> > +\tconst char *file,\n> > +\tconst char *function,\n> > +\tsize_t line,\n> > +\tconst char *err,\n> > +\tint should_abort,\n> > +\tconst char *fmt,\n> > +\t...)\n> > +{\n> > +\tva_list args;\n> > +\tchar buf[4096];\n> > +\tint is_equal = 1;\n> > +\n> > +\tva_start(args, fmt);\n> > +\n> > +\tif (!strcmp(\"%s\", fmt)) {\n> > +\t\tconst char *s1 = va_arg(args, const char *);\n> > +\t\tconst char *s2 = va_arg(args, const char *);\n> > +\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n> > +\n> > +\t\tif (!is_equal) {\n> > +\t\t\tif (s1 && s2) {\n> > +\t\t\t\tint pos;\n> > +\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] &&\n> > s2[pos]; ++pos)\n> > +\t\t\t\t\t/* find differing byte offset */;\n> > +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at\n> > byte %d)\",\n> > +\t\t\t\t\ts1, s2, pos);\n> > +\t\t\t} else {\n> > +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\",\n> > s1, s2);\n> > +\t\t\t}\n> > +\t\t}\n> > +\t}\n> > +\telse if(!strcmp(\"%.*s\", fmt)) {\n> > +\t\tconst char *s1 = va_arg(args, const char *);\n> > +\t\tconst char *s2 = va_arg(args, const char *);\n> > +\t\tint len = va_arg(args, int);\n> > +\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n> > +\n> > +\t\tif (!is_equal) {\n> > +\t\t\tif (s1 && s2) {\n> > +\t\t\t\tint pos;\n> > +\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len;\n> > ++pos)\n> > +\t\t\t\t\t/* find differing byte offset */;\n> > +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\n> > (at byte %d)\",\n> > +\t\t\t\t\tlen, s1, len, s2, pos);\n> > +\t\t\t} else {\n> > +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' !=\n> > '%.*s'\", len, s1, len, s2);\n> > +\t\t\t}\n> > +\t\t}\n> > +\t}\n> > +\telse if (!strcmp(\"%ls\", fmt)) {\n> > +\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n> > +\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n> > +\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1,\n> > wcs2);\n> > +\n> > +\t\tif (!is_equal) {\n> > +\t\t\tif (wcs1 && wcs2) {\n> > +\t\t\t\tint pos;\n> > +\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] &&\n> > wcs1[pos] && wcs2[pos]; ++pos)\n> > +\t\t\t\t\t/* find differing byte offset */;\n> > +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\n> > (at byte %d)\",\n> > +\t\t\t\t\twcs1, wcs2, pos);\n> > +\t\t\t} else {\n> > +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\",\n> > wcs1, wcs2);\n> > +\t\t\t}\n> > +\t\t}\n> > +\t}\n> > +\telse if(!strcmp(\"%.*ls\", fmt)) {\n> > +\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n> > +\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n> > +\t\tint len = va_arg(args, int);\n> > +\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1,\n> > wcs2, len);\n> > +\n> > +\t\tif (!is_equal) {\n> > +\t\t\tif (wcs1 && wcs2) {\n> > +\t\t\t\tint pos;\n> > +\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos <\n> > len; ++pos)\n> > +\t\t\t\t\t/* find differing byte offset */;\n> > +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' !=\n> > '%.*ls' (at byte %d)\",\n> > +\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n> > +\t\t\t} else {\n> > +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' !=\n> > '%.*ls'\", len, wcs1, len, wcs2);\n> > +\t\t\t}\n> > +\t\t}\n> > +\t}\n> > +\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n> > +\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n> > +\t\tis_equal = (sz1 == sz2);\n> > +\t\tif (!is_equal) {\n> > +\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n> > +\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n> > +\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4,\n> > fmt, sz2);\n> > +\t\t}\n> > +\t}\n> > +\telse if (!strcmp(\"%p\", fmt)) {\n> > +\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n> > +\t\tis_equal = (p1 == p2);\n> > +\t\tif (!is_equal)\n> > +\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n> > +\t}\n> > +\telse {\n> > +\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n> > +\t\tis_equal = (i1 == i2);\n> > +\t\tif (!is_equal) {\n> > +\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n> > +\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n> > +\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4,\n> > fmt, i2);\n> > +\t\t}\n> > +\t}\n> > +\n> > +\tva_end(args);\n> > +\n> > +\tif (!is_equal)\n> > +\t\tclar__fail(file, function, line, err, buf, should_abort);\n> > +}\n> > +\n> > +void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n> > +{\n> > +\t_clar.local_cleanup = cleanup;\n> > +\t_clar.local_cleanup_payload = opaque;\n> > +}\n> > +\n> > +#include \"clar/sandbox.h\"\n> > +#include \"clar/fixtures.h\"\n> > +#include \"clar/fs.h\"\n> > +#include \"clar/print.h\"\n> > +#include \"clar/summary.h\"\n> > diff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\n> > new file mode 100644\n> > index 0000000000..8c22382bd5\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/clar.h\n> > @@ -0,0 +1,173 @@\n> > +/*\n> > + * Copyright (c) Vicent Marti. All rights reserved.\n> > + *\n> > + * This file is part of clar, distributed under the ISC license.\n> > + * For full terms see the included COPYING file.\n> > + */\n> > +#ifndef __CLAR_TEST_H__\n> > +#define __CLAR_TEST_H__\n> > +\n> > +#include <stdlib.h>\n> > +\n> > +enum cl_test_status {\n> > +\tCL_TEST_OK,\n> > +\tCL_TEST_FAILURE,\n> > +\tCL_TEST_SKIP,\n> > +\tCL_TEST_NOTRUN,\n> > +};\n> > +\n> > +enum cl_output_format {\n> > +\tCL_OUTPUT_CLAP,\n> > +\tCL_OUTPUT_TAP,\n> > +};\n> > +\n> > +/** Setup clar environment */\n> > +void clar_test_init(int argc, char *argv[]);\n> > +int clar_test_run(void);\n> > +void clar_test_shutdown(void);\n> > +\n> > +/** One shot setup & run */\n> > +int clar_test(int argc, char *argv[]);\n> > +\n> > +const char *clar_sandbox_path(void);\n> > +\n> > +void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n> > +void cl_fs_cleanup(void);\n> > +\n> > +/**\n> > + * cl_trace_* is a hook to provide a simple global tracing\n> > + * mechanism.\n> > + *\n> > + * The goal here is to let main() provide clar-proper\n> > + * with a callback to optionally write log info for\n> > + * test operations into the same stream used by their\n> > + * actual tests.  This would let them print test names\n> > + * and maybe performance data as they choose.\n> > + *\n> > + * The goal is NOT to alter the flow of control or to\n> > + * override test selection/skipping.  (So the callback\n> > + * does not return a value.)\n> > + *\n> > + * The goal is NOT to duplicate the existing\n> > + * pass/fail/skip reporting.  (So the callback\n> > + * does not accept a status/errorcode argument.)\n> > + *\n> > + */\n> > +typedef enum cl_trace_event {\n> > +\tCL_TRACE__SUITE_BEGIN,\n> > +\tCL_TRACE__SUITE_END,\n> > +\tCL_TRACE__TEST__BEGIN,\n> > +\tCL_TRACE__TEST__END,\n> > +\tCL_TRACE__TEST__RUN_BEGIN,\n> > +\tCL_TRACE__TEST__RUN_END,\n> > +\tCL_TRACE__TEST__LONGJMP,\n> > +} cl_trace_event;\n> > +\n> > +typedef void (cl_trace_cb)(\n> > +\tcl_trace_event ev,\n> > +\tconst char *suite_name,\n> > +\tconst char *test_name,\n> > +\tvoid *payload);\n> > +\n> > +/**\n> > + * Register a callback into CLAR to send global trace events.\n> > + * Pass NULL to disable.\n> > + */\n> > +void cl_trace_register(cl_trace_cb *cb, void *payload);\n> > +\n> > +\n> > +#ifdef CLAR_FIXTURE_PATH\n> > +const char *cl_fixture(const char *fixture_name);\n> > +void cl_fixture_sandbox(const char *fixture_name);\n> > +void cl_fixture_cleanup(const char *fixture_name);\n> > +const char *cl_fixture_basename(const char *fixture_name);\n> > +#endif\n> > +\n> > +/**\n> > + * Assertion macros with explicit error message\n> > + */\n> > +#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__,\n> > __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n> > +#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__,\n> > __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n> > +#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__,\n> > __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n> > +\n> > +/**\n> > + * Check macros with explicit error message\n> > + */\n> > +#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__,\n> > __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n> > +#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__,\n> > __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n> > +#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__,\n> > __LINE__, \"Expression is not true: \" #expr, desc, 0)\n> > +\n> > +/**\n> > + * Assertion macros with no error message\n> > + */\n> > +#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n> > +#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n> > +#define cl_assert(expr) cl_assert_(expr, NULL)\n> > +\n> > +/**\n> > + * Check macros with no error message\n> > + */\n> > +#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n> > +#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n> > +#define cl_check(expr) cl_check_(expr, NULL)\n> > +\n> > +/**\n> > + * Forced failure/warning\n> > + */\n> > +#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test\n> > failed.\", desc, 1)\n> > +#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning\n> > during test execution:\", desc, 0)\n> > +\n> > +#define cl_skip() clar__skip()\n> > +\n> > +/**\n> > + * Typed assertion macros\n> > + */\n> > +#define cl_assert_equal_s(s1,s2)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \"\n> > #s2, 1, \"%s\", (s1), (s2))\n> > +#define cl_assert_equal_s_(s1,s2,note)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \"\n> > #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n> > +\n> > +#define cl_assert_equal_wcs(wcs1,wcs2)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" !=\n> > \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n> > +#define cl_assert_equal_wcs_(wcs1,wcs2,note)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" !=\n> > \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n> > +\n> > +#define cl_assert_equal_strn(s1,s2,len)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \"\n> > #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n> > +#define cl_assert_equal_strn_(s1,s2,len,note)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \"\n> > #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n> > +\n> > +#define cl_assert_equal_wcsn(wcs1,wcs2,len)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" !=\n> > \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n> > +#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" !=\n> > \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n> > +\n> > +#define cl_assert_equal_i(i1,i2)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\",\n> > (int)(i1), (int)(i2))\n> > +#define cl_assert_equal_i_(i1,i2,note)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\",\n> > 1, \"%d\", (i1), (i2))\n> > +#define cl_assert_equal_i_fmt(i1,i2,fmt)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt),\n> > (int)(i1), (int)(i2))\n> > +\n> > +#define cl_assert_equal_b(b1,b2)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\",\n> > (int)((b1) != 0),(int)((b2) != 0))\n> > +\n> > +#define cl_assert_equal_p(p1,p2)\n> > clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" !=\n> > \" #p2, 1, \"%p\", (p1), (p2))\n> > +\n> > +void clar__skip(void);\n> > +\n> > +void clar__fail(\n> > +\tconst char *file,\n> > +\tconst char *func,\n> > +\tsize_t line,\n> > +\tconst char *error,\n> > +\tconst char *description,\n> > +\tint should_abort);\n> > +\n> > +void clar__assert(\n> > +\tint condition,\n> > +\tconst char *file,\n> > +\tconst char *func,\n> > +\tsize_t line,\n> > +\tconst char *error,\n> > +\tconst char *description,\n> > +\tint should_abort);\n> > +\n> > +void clar__assert_equal(\n> > +\tconst char *file,\n> > +\tconst char *func,\n> > +\tsize_t line,\n> > +\tconst char *err,\n> > +\tint should_abort,\n> > +\tconst char *fmt,\n> > +\t...);\n> > +\n> > +#endif\n> > diff --git a/t/unit-tests/clar/clar/fixtures.h\n> > b/t/unit-tests/clar/clar/fixtures.h\n> > new file mode 100644\n> > index 0000000000..6ec6423484\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/clar/fixtures.h\n> > @@ -0,0 +1,50 @@\n> > +#ifdef CLAR_FIXTURE_PATH\n> > +static const char *\n> > +fixture_path(const char *base, const char *fixture_name)\n> > +{\n> > +\tstatic char _path[4096];\n> > +\tsize_t root_len;\n> > +\n> > +\troot_len = strlen(base);\n> > +\tstrncpy(_path, base, sizeof(_path));\n> > +\n> > +\tif (_path[root_len - 1] != '/')\n> > +\t\t_path[root_len++] = '/';\n> > +\n> > +\tif (fixture_name[0] == '/')\n> > +\t\tfixture_name++;\n> > +\n> > +\tstrncpy(_path + root_len,\n> > +\t\tfixture_name,\n> > +\t\tsizeof(_path) - root_len);\n> > +\n> > +\treturn _path;\n> > +}\n> > +\n> > +const char *cl_fixture(const char *fixture_name)\n> > +{\n> > +\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n> > +}\n> > +\n> > +void cl_fixture_sandbox(const char *fixture_name)\n> > +{\n> > +\tfs_copy(cl_fixture(fixture_name), _clar_path);\n> > +}\n> > +\n> > +const char *cl_fixture_basename(const char *fixture_name)\n> > +{\n> > +\tconst char *p;\n> > +\n> > +\tfor (p = fixture_name; *p; p++) {\n> > +\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n> > +\t\t\tfixture_name = p+1;\n> > +\t}\n> > +\n> > +\treturn fixture_name;\n> > +}\n> > +\n> > +void cl_fixture_cleanup(const char *fixture_name)\n> > +{\n> > +\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n> > +}\n> > +#endif\n> > diff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\n> > new file mode 100644\n> > index 0000000000..3e39890bd3\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/clar/fs.h\n> > @@ -0,0 +1,522 @@\n> > +/*\n> > + * By default, use a read/write loop to copy files on POSIX systems.\n> > + * On Linux, use sendfile by default as it's slightly faster.  On\n> > + * macOS, we avoid fcopyfile by default because it's slightly slower.\n> > + */\n> > +#undef USE_FCOPYFILE\n> > +#define USE_SENDFILE 1\n> > +\n> > +#ifdef _WIN32\n> > +\n> > +#ifdef CLAR_WIN32_LONGPATHS\n> > +# define CLAR_MAX_PATH 4096\n> > +#else\n> > +# define CLAR_MAX_PATH MAX_PATH\n> > +#endif\n> > +\n> > +#define RM_RETRY_COUNT\t5\n> > +#define RM_RETRY_DELAY\t10\n> > +\n> > +#ifdef __MINGW32__\n> > +\n> > +/* These security-enhanced functions are not available\n> > + * in MinGW, so just use the vanilla ones */\n> > +#define wcscpy_s(a, b, c) wcscpy((a), (c))\n> > +#define wcscat_s(a, b, c) wcscat((a), (c))\n> > +\n> > +#endif /* __MINGW32__ */\n> > +\n> > +static int\n> > +fs__dotordotdot(WCHAR *_tocheck)\n> > +{\n> > +\treturn _tocheck[0] == '.' &&\n> > +\t\t(_tocheck[1] == '\\0' ||\n> > +\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n> > +}\n> > +\n> > +static int\n> > +fs_rmdir_rmdir(WCHAR *_wpath)\n> > +{\n> > +\tunsigned retries = 1;\n> > +\n> > +\twhile (!RemoveDirectoryW(_wpath)) {\n> > +\t\t/* Only retry when we have retries remaining, and the\n> > +\t\t * error was ERROR_DIR_NOT_EMPTY. */\n> > +\t\tif (retries++ > RM_RETRY_COUNT ||\n> > +\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n> > +\t\t\treturn -1;\n> > +\n> > +\t\t/* Give whatever has a handle to a child item some time\n> > +\t\t * to release it before trying again */\n> > +\t\tSleep(RM_RETRY_DELAY * retries * retries);\n> > +\t}\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +static void translate_path(WCHAR *path, size_t path_size)\n> > +{\n> > +    size_t path_len, i;\n> > +\n> > +    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n> > +\treturn;\n> > +\n> > +    path_len = wcslen(path);\n> > +    cl_assert(path_size > path_len + 4);\n> > +\n> > +    for (i = path_len; i > 0; i--) {\n> > +\tWCHAR c = path[i - 1];\n> > +\n> > +\tif (c == L'/')\n> > +\t    path[i + 3] = L'\\\\';\n> > +\telse\n> > +\t    path[i + 3] = path[i - 1];\n> > +    }\n> > +\n> > +    path[0] = L'\\\\';\n> > +    path[1] = L'\\\\';\n> > +    path[2] = L'?';\n> > +    path[3] = L'\\\\';\n> > +    path[path_len + 4] = L'\\0';\n> > +}\n> > +\n> > +static void\n> > +fs_rmdir_helper(WCHAR *_wsource)\n> > +{\n> > +\tWCHAR buffer[CLAR_MAX_PATH];\n> > +\tHANDLE find_handle;\n> > +\tWIN32_FIND_DATAW find_data;\n> > +\tsize_t buffer_prefix_len;\n> > +\n> > +\t/* Set up the buffer and capture the length */\n> > +\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n> > +\ttranslate_path(buffer, CLAR_MAX_PATH);\n> > +\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n> > +\tbuffer_prefix_len = wcslen(buffer);\n> > +\n> > +\t/* FindFirstFile needs a wildcard to match multiple items */\n> > +\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n> > +\tfind_handle = FindFirstFileW(buffer, &find_data);\n> > +\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n> > +\n> > +\tdo {\n> > +\t\t/* FindFirstFile/FindNextFile gives back . and ..\n> > +\t\t * entries at the beginning */\n> > +\t\tif (fs__dotordotdot(find_data.cFileName))\n> > +\t\t\tcontinue;\n> > +\n> > +\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH -\n> > buffer_prefix_len, find_data.cFileName);\n> > +\n> > +\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n> > +\t\t\tfs_rmdir_helper(buffer);\n> > +\t\telse {\n> > +\t\t\t/* If set, the +R bit must be cleared before deleting\n> > */\n> > +\t\t\tif (FILE_ATTRIBUTE_READONLY &\n> > find_data.dwFileAttributes)\n> > +\t\t\t\tcl_assert(SetFileAttributesW(buffer,\n> > find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n> > +\n> > +\t\t\tcl_assert(DeleteFileW(buffer));\n> > +\t\t}\n> > +\t}\n> > +\twhile (FindNextFileW(find_handle, &find_data));\n> > +\n> > +\t/* Ensure that we successfully completed the enumeration */\n> > +\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n> > +\n> > +\t/* Close the find handle */\n> > +\tFindClose(find_handle);\n> > +\n> > +\t/* Now that the directory is empty, remove it */\n> > +\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n> > +}\n> > +\n> > +static int\n> > +fs_rm_wait(WCHAR *_wpath)\n> > +{\n> > +\tunsigned retries = 1;\n> > +\tDWORD last_error;\n> > +\n> > +\tdo {\n> > +\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n> > +\t\t\tlast_error = GetLastError();\n> > +\t\telse\n> > +\t\t\tlast_error = ERROR_SUCCESS;\n> > +\n> > +\t\t/* Is the item gone? */\n> > +\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n> > +\t\t\tERROR_PATH_NOT_FOUND == last_error)\n> > +\t\t\treturn 0;\n> > +\n> > +\t\tSleep(RM_RETRY_DELAY * retries * retries);\n> > +\t}\n> > +\twhile (retries++ <= RM_RETRY_COUNT);\n> > +\n> > +\treturn -1;\n> > +}\n> > +\n> > +static void\n> > +fs_rm(const char *_source)\n> > +{\n> > +\tWCHAR wsource[CLAR_MAX_PATH];\n> > +\tDWORD attrs;\n> > +\n> > +\t/* The input path is UTF-8. Convert it to wide characters\n> > +\t * for use with the Windows API */\n> > +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n> > +\t\t\t\tMB_ERR_INVALID_CHARS,\n> > +\t\t\t\t_source,\n> > +\t\t\t\t-1, /* Indicates NULL termination */\n> > +\t\t\t\twsource,\n> > +\t\t\t\tCLAR_MAX_PATH));\n> > +\n> > +\ttranslate_path(wsource, CLAR_MAX_PATH);\n> > +\n> > +\t/* Does the item exist? If not, we have no work to do */\n> > +\tattrs = GetFileAttributesW(wsource);\n> > +\n> > +\tif (INVALID_FILE_ATTRIBUTES == attrs)\n> > +\t\treturn;\n> > +\n> > +\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n> > +\t\tfs_rmdir_helper(wsource);\n> > +\telse {\n> > +\t\t/* The item is a file. Strip the +R bit */\n> > +\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n> > +\t\t\tcl_assert(SetFileAttributesW(wsource, attrs &\n> > ~FILE_ATTRIBUTE_READONLY));\n> > +\n> > +\t\tcl_assert(DeleteFileW(wsource));\n> > +\t}\n> > +\n> > +\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n> > +\tcl_assert(0 == fs_rm_wait(wsource));\n> > +}\n> > +\n> > +static void\n> > +fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n> > +{\n> > +\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n> > +\tHANDLE find_handle;\n> > +\tWIN32_FIND_DATAW find_data;\n> > +\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n> > +\n> > +\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n> > +\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n> > +\ttranslate_path(buf_source, CLAR_MAX_PATH);\n> > +\tbuf_source_prefix_len = wcslen(buf_source);\n> > +\n> > +\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n> > +\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n> > +\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n> > +\tbuf_dest_prefix_len = wcslen(buf_dest);\n> > +\n> > +\t/* Get an enumerator for the items in the source. */\n> > +\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n> > +\tfind_handle = FindFirstFileW(buf_source, &find_data);\n> > +\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n> > +\n> > +\t/* Create the target directory. */\n> > +\tcl_assert(CreateDirectoryW(_wdest, NULL));\n> > +\n> > +\tdo {\n> > +\t\t/* FindFirstFile/FindNextFile gives back . and ..\n> > +\t\t * entries at the beginning */\n> > +\t\tif (fs__dotordotdot(find_data.cFileName))\n> > +\t\t\tcontinue;\n> > +\n> > +\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH -\n> > buf_source_prefix_len, find_data.cFileName);\n> > +\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH -\n> > buf_dest_prefix_len, find_data.cFileName);\n> > +\n> > +\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n> > +\t\t\tfs_copydir_helper(buf_source, buf_dest);\n> > +\t\telse\n> > +\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n> > +\t}\n> > +\twhile (FindNextFileW(find_handle, &find_data));\n> > +\n> > +\t/* Ensure that we successfully completed the enumeration */\n> > +\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n> > +\n> > +\t/* Close the find handle */\n> > +\tFindClose(find_handle);\n> > +}\n> > +\n> > +static void\n> > +fs_copy(const char *_source, const char *_dest)\n> > +{\n> > +\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n> > +\tDWORD source_attrs, dest_attrs;\n> > +\tHANDLE find_handle;\n> > +\tWIN32_FIND_DATAW find_data;\n> > +\n> > +\t/* The input paths are UTF-8. Convert them to wide characters\n> > +\t * for use with the Windows API. */\n> > +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n> > +\t\t\t\tMB_ERR_INVALID_CHARS,\n> > +\t\t\t\t_source,\n> > +\t\t\t\t-1,\n> > +\t\t\t\twsource,\n> > +\t\t\t\tCLAR_MAX_PATH));\n> > +\n> > +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n> > +\t\t\t\tMB_ERR_INVALID_CHARS,\n> > +\t\t\t\t_dest,\n> > +\t\t\t\t-1,\n> > +\t\t\t\twdest,\n> > +\t\t\t\tCLAR_MAX_PATH));\n> > +\n> > +\ttranslate_path(wsource, CLAR_MAX_PATH);\n> > +\ttranslate_path(wdest, CLAR_MAX_PATH);\n> > +\n> > +\t/* Check the source for existence */\n> > +\tsource_attrs = GetFileAttributesW(wsource);\n> > +\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n> > +\n> > +\t/* Check the target for existence */\n> > +\tdest_attrs = GetFileAttributesW(wdest);\n> > +\n> > +\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n> > +\t\t/* Target exists; append last path part of source to target.\n> > +\t\t * Use FindFirstFile to parse the path */\n> > +\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n> > +\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n> > +\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n> > +\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n> > +\t\tFindClose(find_handle);\n> > +\n> > +\t\t/* Check the new target for existence */\n> > +\t\tcl_assert(INVALID_FILE_ATTRIBUTES ==\n> > GetFileAttributesW(wdest));\n> > +\t}\n> > +\n> > +\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n> > +\t\tfs_copydir_helper(wsource, wdest);\n> > +\telse\n> > +\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n> > +}\n> > +\n> > +void\n> > +cl_fs_cleanup(void)\n> > +{\n> > +#ifdef CLAR_FIXTURE_PATH\n> > +\tfs_rm(fixture_path(_clar_path, \"*\"));\n> > +#endif\n> > +}\n> > +\n> > +#else\n> > +\n> > +#include <errno.h>\n> > +#include <string.h>\n> > +#include <limits.h>\n> > +#include <dirent.h>\n> > +#include <fcntl.h>\n> > +#include <unistd.h>\n> > +#include <sys/types.h>\n> > +#include <sys/stat.h>\n> > +\n> > +#if defined(__linux__)\n> > +# include <sys/sendfile.h>\n> > +#endif\n> > +\n> > +#if defined(__APPLE__)\n> > +# include <copyfile.h>\n> > +#endif\n> > +\n> > +static void basename_r(const char **out, int *out_len, const char *in)\n> > +{\n> > +\tsize_t in_len = strlen(in), start_pos;\n> > +\n> > +\tfor (in_len = strlen(in); in_len; in_len--) {\n> > +\t\tif (in[in_len - 1] != '/')\n> > +\t\t\tbreak;\n> > +\t}\n> > +\n> > +\tfor (start_pos = in_len; start_pos; start_pos--) {\n> > +\t\tif (in[start_pos - 1] == '/')\n> > +\t\t\tbreak;\n> > +\t}\n> > +\n> > +\tcl_assert(in_len - start_pos < INT_MAX);\n> > +\n> > +\tif (in_len - start_pos > 0) {\n> > +\t\t*out = &in[start_pos];\n> > +\t\t*out_len = (in_len - start_pos);\n> > +\t} else {\n> > +\t\t*out = \"/\";\n> > +\t\t*out_len = 1;\n> > +\t}\n> > +}\n> > +\n> > +static char *joinpath(const char *dir, const char *base, int base_len)\n> > +{\n> > +\tchar *out;\n> > +\tint len;\n> > +\n> > +\tif (base_len == -1) {\n> > +\t\tsize_t bl = strlen(base);\n> > +\n> > +\t\tcl_assert(bl < INT_MAX);\n> > +\t\tbase_len = (int)bl;\n> > +\t}\n> > +\n> > +\tlen = strlen(dir) + base_len + 2;\n> > +\tcl_assert(len > 0);\n> > +\n> > +\tcl_assert(out = malloc(len));\n> > +\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n> > +\n> > +\treturn out;\n> > +}\n> > +\n> > +static void\n> > +fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n> > +{\n> > +\tDIR *source_dir;\n> > +\tstruct dirent *d;\n> > +\n> > +\tmkdir(dest, dest_mode);\n> > +\n> > +\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n> > +\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n> > +\t\tchar *child;\n> > +\n> > +\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n> > +\t\t\tcontinue;\n> > +\n> > +\t\tchild = joinpath(source, d->d_name, -1);\n> > +\t\tfs_copy(child, dest);\n> > +\t\tfree(child);\n> > +\t}\n> > +\n> > +\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n> > +\n> > +\tclosedir(source_dir);\n> > +}\n> > +\n> > +static void\n> > +fs_copyfile_helper(const char *source, size_t source_len, const char *dest,\n> > int dest_mode)\n> > +{\n> > +\tint in, out;\n> > +\n> > +\tcl_must_pass((in = open(source, O_RDONLY)));\n> > +\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n> > +\n> > +#if USE_FCOPYFILE && defined(__APPLE__)\n> > +\t((void)(source_len)); /* unused */\n> > +\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n> > +#elif USE_SENDFILE && defined(__linux__)\n> > +\t{\n> > +\t\tssize_t ret = 0;\n> > +\n> > +\t\twhile (source_len && (ret = sendfile(out, in, NULL,\n> > source_len)) > 0) {\n> > +\t\t\tsource_len -= (size_t)ret;\n> > +\t\t}\n> > +\t\tcl_assert(ret >= 0);\n> > +\t}\n> > +#else\n> > +\t{\n> > +\t\tchar buf[131072];\n> > +\t\tssize_t ret;\n> > +\n> > +\t\t((void)(source_len)); /* unused */\n> > +\n> > +\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n> > +\t\t\tsize_t len = (size_t)ret;\n> > +\n> > +\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n> > +\t\t\t\tcl_assert(ret <= (ssize_t)len);\n> > +\t\t\t\tlen -= ret;\n> > +\t\t\t}\n> > +\t\t\tcl_assert(ret >= 0);\n> > +\t\t}\n> > +\t\tcl_assert(ret == 0);\n> > +\t}\n> > +#endif\n> > +\n> > +\tclose(in);\n> > +\tclose(out);\n> > +}\n> > +\n> > +static void\n> > +fs_copy(const char *source, const char *_dest)\n> > +{\n> > +\tchar *dbuf = NULL;\n> > +\tconst char *dest = NULL;\n> > +\tstruct stat source_st, dest_st;\n> > +\n> > +\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy\n> > source\");\n> > +\n> > +\tif (lstat(_dest, &dest_st) == 0) {\n> > +\t\tconst char *base;\n> > +\t\tint base_len;\n> > +\n> > +\t\t/* Target exists and is directory; append basename */\n> > +\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n> > +\n> > +\t\tbasename_r(&base, &base_len, source);\n> > +\t\tcl_assert(base_len < INT_MAX);\n> > +\n> > +\t\tdbuf = joinpath(_dest, base, base_len);\n> > +\t\tdest = dbuf;\n> > +\t} else if (errno != ENOENT) {\n> > +\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n> > +\t} else {\n> > +\t\tdest = _dest;\n> > +\t}\n> > +\n> > +\tif (S_ISDIR(source_st.st_mode)) {\n> > +\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n> > +\t} else {\n> > +\t\tfs_copyfile_helper(source, source_st.st_size, dest,\n> > source_st.st_mode);\n> > +\t}\n> > +\n> > +\tfree(dbuf);\n> > +}\n> > +\n> > +static void\n> > +fs_rmdir_helper(const char *path)\n> > +{\n> > +\tDIR *dir;\n> > +\tstruct dirent *d;\n> > +\n> > +\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n> > +\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n> > +\t\tchar *child;\n> > +\n> > +\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n> > +\t\t\tcontinue;\n> > +\n> > +\t\tchild = joinpath(path, d->d_name, -1);\n> > +\t\tfs_rm(child);\n> > +\t\tfree(child);\n> > +\t}\n> > +\n> > +\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n> > +\tclosedir(dir);\n> > +\n> > +\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n> > +}\n> > +\n> > +static void\n> > +fs_rm(const char *path)\n> > +{\n> > +\tstruct stat st;\n> > +\n> > +\tif (lstat(path, &st)) {\n> > +\t\tif (errno == ENOENT)\n> > +\t\t\treturn;\n> > +\n> > +\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n> > +\t}\n> > +\n> > +\tif (S_ISDIR(st.st_mode)) {\n> > +\t\tfs_rmdir_helper(path);\n> > +\t} else {\n> > +\t\tcl_must_pass(unlink(path));\n> > +\t}\n> > +}\n> > +\n> > +void\n> > +cl_fs_cleanup(void)\n> > +{\n> > +\tclar_unsandbox();\n> > +\tclar_sandbox();\n> > +}\n> > +#endif\n> > diff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\n> > new file mode 100644\n> > index 0000000000..c17e2f693b\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/clar/print.h\n> > @@ -0,0 +1,211 @@\n> > +/* clap: clar protocol, the traditional clar output format */\n> > +\n> > +static void clar_print_clap_init(int test_count, int suite_count, const\n> > char *suite_names)\n> > +{\n> > +\t(void)test_count;\n> > +\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n> > +\tprintf(\"Started (test status codes: OK='.' FAILURE='F'\n> > SKIPPED='S')\\n\");\n> > +}\n> > +\n> > +static void clar_print_clap_shutdown(int test_count, int suite_count, int\n> > error_count)\n> > +{\n> > +\t(void)test_count;\n> > +\t(void)suite_count;\n> > +\t(void)error_count;\n> > +\n> > +\tprintf(\"\\n\\n\");\n> > +\tclar_report_all();\n> > +}\n> > +\n> > +static void clar_print_clap_error(int num, const struct clar_report\n> > *report, const struct clar_error *error)\n> > +{\n> > +\tprintf(\"  %d) Failure:\\n\", num);\n> > +\n> > +\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n> > +\t\treport->suite,\n> > +\t\treport->test,\n> > +\t\terror->file,\n> > +\t\terror->line_number);\n> > +\n> > +\tprintf(\"  %s\\n\", error->error_msg);\n> > +\n> > +\tif (error->description != NULL)\n> > +\t\tprintf(\"  %s\\n\", error->description);\n> > +\n> > +\tprintf(\"\\n\");\n> > +\tfflush(stdout);\n> > +}\n> > +\n> > +static void clar_print_clap_ontest(const char *suite_name, const char\n> > *test_name, int test_number, enum cl_test_status status)\n> > +{\n> > +\t(void)test_name;\n> > +\t(void)test_number;\n> > +\n> > +\tif (_clar.verbosity > 1) {\n> > +\t\tprintf(\"%s::%s: \", suite_name, test_name);\n> > +\n> > +\t\tswitch (status) {\n> > +\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n> > +\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n> > +\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n> > +\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n> > +\t\t}\n> > +\t} else {\n> > +\t\tswitch (status) {\n> > +\t\tcase CL_TEST_OK: printf(\".\"); break;\n> > +\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n> > +\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n> > +\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n> > +\t\t}\n> > +\n> > +\t\tfflush(stdout);\n> > +\t}\n> > +}\n> > +\n> > +static void clar_print_clap_onsuite(const char *suite_name, int\n> > suite_index)\n> > +{\n> > +\tif (_clar.verbosity == 1)\n> > +\t\tprintf(\"\\n%s\", suite_name);\n> > +\n> > +\t(void)suite_index;\n> > +}\n> > +\n> > +static void clar_print_clap_onabort(const char *fmt, va_list arg)\n> > +{\n> > +\tvfprintf(stderr, fmt, arg);\n> > +}\n> > +\n> > +/* tap: test anywhere protocol format */\n> > +\n> > +static void clar_print_tap_init(int test_count, int suite_count, const char\n> > *suite_names)\n> > +{\n> > +\t(void)test_count;\n> > +\t(void)suite_count;\n> > +\t(void)suite_names;\n> > +\tprintf(\"TAP version 13\\n\");\n> > +}\n> > +\n> > +static void clar_print_tap_shutdown(int test_count, int suite_count, int\n> > error_count)\n> > +{\n> > +\t(void)suite_count;\n> > +\t(void)error_count;\n> > +\n> > +\tprintf(\"1..%d\\n\", test_count);\n> > +}\n> > +\n> > +static void clar_print_tap_error(int num, const struct clar_report *report,\n> > const struct clar_error *error)\n> > +{\n> > +\t(void)num;\n> > +\t(void)report;\n> > +\t(void)error;\n> > +}\n> > +\n> > +static void print_escaped(const char *str)\n> > +{\n> > +\tchar *c;\n> > +\n> > +\twhile ((c = strchr(str, '\\'')) != NULL) {\n> > +\t\tprintf(\"%.*s\", (int)(c - str), str);\n> > +\t\tprintf(\"''\");\n> > +\t\tstr = c + 1;\n> > +\t}\n> > +\n> > +\tprintf(\"%s\", str);\n> > +}\n> > +\n> > +static void clar_print_tap_ontest(const char *suite_name, const char\n> > *test_name, int test_number, enum cl_test_status status)\n> > +{\n> > +\tconst struct clar_error *error = _clar.last_report->errors;\n> > +\n> > +\t(void)test_name;\n> > +\t(void)test_number;\n> > +\n> > +\tswitch(status) {\n> > +\tcase CL_TEST_OK:\n> > +\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name,\n> > test_name);\n> > +\t\tbreak;\n> > +\tcase CL_TEST_FAILURE:\n> > +\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name,\n> > test_name);\n> > +\n> > +\t\tprintf(\"    ---\\n\");\n> > +\t\tprintf(\"    reason: |\\n\");\n> > +\t\tprintf(\"      %s\\n\", error->error_msg);\n> > +\n> > +\t\tif (error->description)\n> > +\t\t\tprintf(\"      %s\\n\", error->description);\n> > +\n> > +\t\tprintf(\"    at:\\n\");\n> > +\t\tprintf(\"      file: '\"); print_escaped(error->file);\n> > printf(\"'\\n\");\n> > +\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n> > +\t\tprintf(\"      function: '%s'\\n\", error->function);\n> > +\t\tprintf(\"    ---\\n\");\n> > +\n> > +\t\tbreak;\n> > +\tcase CL_TEST_SKIP:\n> > +\tcase CL_TEST_NOTRUN:\n> > +\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name,\n> > test_name);\n> > +\t\tbreak;\n> > +\t}\n> > +\n> > +\tfflush(stdout);\n> > +}\n> > +\n> > +static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n> > +{\n> > +\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n> > +}\n> > +\n> > +static void clar_print_tap_onabort(const char *fmt, va_list arg)\n> > +{\n> > +\tprintf(\"Bail out! \");\n> > +\tvprintf(fmt, arg);\n> > +\tfflush(stdout);\n> > +}\n> > +\n> > +/* indirection between protocol output selection */\n> > +\n> > +#define PRINT(FN, ...) do { \\\n> > +\t\tswitch (_clar.output_format) { \\\n> > +\t\t\tcase CL_OUTPUT_CLAP: \\\n> > +\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n> > +\t\t\t\tbreak; \\\n> > +\t\t\tcase CL_OUTPUT_TAP: \\\n> > +\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n> > +\t\t\t\tbreak; \\\n> > +\t\t\tdefault: \\\n> > +\t\t\t\tabort(); \\\n> > +\t\t} \\\n> > +\t} while (0)\n> > +\n> > +static void clar_print_init(int test_count, int suite_count, const char\n> > *suite_names)\n> > +{\n> > +\tPRINT(init, test_count, suite_count, suite_names);\n> > +}\n> > +\n> > +static void clar_print_shutdown(int test_count, int suite_count, int\n> > error_count)\n> > +{\n> > +\tPRINT(shutdown, test_count, suite_count, error_count);\n> > +}\n> > +\n> > +static void clar_print_error(int num, const struct clar_report *report,\n> > const struct clar_error *error)\n> > +{\n> > +\tPRINT(error, num, report, error);\n> > +}\n> > +\n> > +static void clar_print_ontest(const char *suite_name, const char\n> > *test_name, int test_number, enum cl_test_status status)\n> > +{\n> > +\tPRINT(ontest, suite_name, test_name, test_number, status);\n> > +}\n> > +\n> > +static void clar_print_onsuite(const char *suite_name, int suite_index)\n> > +{\n> > +\tPRINT(onsuite, suite_name, suite_index);\n> > +}\n> > +\n> > +static void clar_print_onabort(const char *msg, ...)\n> > +{\n> > +\tva_list argp;\n> > +\tva_start(argp, msg);\n> > +\tPRINT(onabort, msg, argp);\n> > +\tva_end(argp);\n> > +}\n> > diff --git a/t/unit-tests/clar/clar/sandbox.h\n> > b/t/unit-tests/clar/clar/sandbox.h\n> > new file mode 100644\n> > index 0000000000..7c177f3525\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/clar/sandbox.h\n> > @@ -0,0 +1,153 @@\n> > +#ifdef __APPLE__\n> > +#include <sys/syslimits.h>\n> > +#endif\n> > +\n> > +static char _clar_path[4096 + 1];\n> > +\n> > +static int\n> > +is_valid_tmp_path(const char *path)\n> > +{\n> > +\tSTAT_T st;\n> > +\n> > +\tif (stat(path, &st) != 0)\n> > +\t\treturn 0;\n> > +\n> > +\tif (!S_ISDIR(st.st_mode))\n> > +\t\treturn 0;\n> > +\n> > +\treturn (access(path, W_OK) == 0);\n> > +}\n> > +\n> > +static int\n> > +find_tmp_path(char *buffer, size_t length)\n> > +{\n> > +#ifndef _WIN32\n> > +\tstatic const size_t var_count = 5;\n> > +\tstatic const char *env_vars[] = {\n> > +\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n> > +\t};\n> > +\n> > +\tsize_t i;\n> > +\n> > +\tfor (i = 0; i < var_count; ++i) {\n> > +\t\tconst char *env = getenv(env_vars[i]);\n> > +\t\tif (!env)\n> > +\t\t\tcontinue;\n> > +\n> > +\t\tif (is_valid_tmp_path(env)) {\n> > +#ifdef __APPLE__\n> > +\t\t\tif (length >= PATH_MAX && realpath(env, buffer) !=\n> > NULL)\n> > +\t\t\t\treturn 0;\n> > +#endif\n> > +\t\t\tstrncpy(buffer, env, length - 1);\n> > +\t\t\tbuffer[length - 1] = '\\0';\n> > +\t\t\treturn 0;\n> > +\t\t}\n> > +\t}\n> > +\n> > +\t/* If the environment doesn't say anything, try to use /tmp */\n> > +\tif (is_valid_tmp_path(\"/tmp\")) {\n> > +#ifdef __APPLE__\n> > +\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n> > +\t\t\treturn 0;\n> > +#endif\n> > +\t\tstrncpy(buffer, \"/tmp\", length - 1);\n> > +\t\tbuffer[length - 1] = '\\0';\n> > +\t\treturn 0;\n> > +\t}\n> > +\n> > +#else\n> > +\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer,\n> > (DWORD)length);\n> > +\tif (env_len > 0 && env_len < (DWORD)length)\n> > +\t\treturn 0;\n> > +\n> > +\tif (GetTempPath((DWORD)length, buffer))\n> > +\t\treturn 0;\n> > +#endif\n> > +\n> > +\t/* This system doesn't like us, try to use the current directory */\n> > +\tif (is_valid_tmp_path(\".\")) {\n> > +\t\tstrncpy(buffer, \".\", length - 1);\n> > +\t\tbuffer[length - 1] = '\\0';\n> > +\t\treturn 0;\n> > +\t}\n> > +\n> > +\treturn -1;\n> > +}\n> > +\n> > +static void clar_unsandbox(void)\n> > +{\n> > +\tif (_clar_path[0] == '\\0')\n> > +\t\treturn;\n> > +\n> > +\tcl_must_pass(chdir(\"..\"));\n> > +\n> > +\tfs_rm(_clar_path);\n> > +}\n> > +\n> > +static int build_sandbox_path(void)\n> > +{\n> > +#ifdef CLAR_TMPDIR\n> > +\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n> > +#else\n> > +\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n> > +#endif\n> > +\n> > +\tsize_t len;\n> > +\n> > +\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n> > +\t\treturn -1;\n> > +\n> > +\tlen = strlen(_clar_path);\n> > +\n> > +#ifdef _WIN32\n> > +\t{ /* normalize path to POSIX forward slashes */\n> > +\t\tsize_t i;\n> > +\t\tfor (i = 0; i < len; ++i) {\n> > +\t\t\tif (_clar_path[i] == '\\\\')\n> > +\t\t\t\t_clar_path[i] = '/';\n> > +\t\t}\n> > +\t}\n> > +#endif\n> > +\n> > +\tif (_clar_path[len - 1] != '/') {\n> > +\t\t_clar_path[len++] = '/';\n> > +\t}\n> > +\n> > +\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n> > +\n> > +#if defined(__MINGW32__)\n> > +\tif (_mktemp(_clar_path) == NULL)\n> > +\t\treturn -1;\n> > +\n> > +\tif (mkdir(_clar_path, 0700) != 0)\n> > +\t\treturn -1;\n> > +#elif defined(_WIN32)\n> > +\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n> > +\t\treturn -1;\n> > +\n> > +\tif (mkdir(_clar_path, 0700) != 0)\n> > +\t\treturn -1;\n> > +#else\n> > +\tif (mkdtemp(_clar_path) == NULL)\n> > +\t\treturn -1;\n> > +#endif\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +static int clar_sandbox(void)\n> > +{\n> > +\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n> > +\t\treturn -1;\n> > +\n> > +\tif (chdir(_clar_path) != 0)\n> > +\t\treturn -1;\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +const char *clar_sandbox_path(void)\n> > +{\n> > +\treturn _clar_path;\n> > +}\n> > diff --git a/t/unit-tests/clar/clar/summary.h\n> > b/t/unit-tests/clar/clar/summary.h\n> > new file mode 100644\n> > index 0000000000..4dd352e28b\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/clar/summary.h\n> > @@ -0,0 +1,143 @@\n> > +\n> > +#include <stdio.h>\n> > +#include <time.h>\n> > +\n> > +static int clar_summary_close_tag(\n> > +    struct clar_summary *summary, const char *tag, int indent)\n> > +{\n> > +\tconst char *indt;\n> > +\n> > +\tif (indent == 0) indt = \"\";\n> > +\telse if (indent == 1) indt = \"\\t\";\n> > +\telse indt = \"\\t\\t\";\n> > +\n> > +\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n> > +}\n> > +\n> > +static int clar_summary_testsuites(struct clar_summary *summary)\n> > +{\n> > +\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n> > +}\n> > +\n> > +static int clar_summary_testsuite(struct clar_summary *summary,\n> > +    int idn, const char *name, time_t timestamp,\n> > +    int test_count, int fail_count, int error_count)\n> > +{\n> > +\tstruct tm *tm = localtime(&timestamp);\n> > +\tchar iso_dt[20];\n> > +\n> > +\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n> > +\t\treturn -1;\n> > +\n> > +\treturn fprintf(summary->fp, \"\\t<testsuite\"\n> > +\t\t       \" id=\\\"%d\\\"\"\n> > +\t\t       \" name=\\\"%s\\\"\"\n> > +\t\t       \" hostname=\\\"localhost\\\"\"\n> > +\t\t       \" timestamp=\\\"%s\\\"\"\n> > +\t\t       \" tests=\\\"%d\\\"\"\n> > +\t\t       \" failures=\\\"%d\\\"\"\n> > +\t\t       \" errors=\\\"%d\\\">\\n\",\n> > +\t\t       idn, name, iso_dt, test_count, fail_count,\n> > error_count);\n> > +}\n> > +\n> > +static int clar_summary_testcase(struct clar_summary *summary,\n> > +    const char *name, const char *classname, double elapsed)\n> > +{\n> > +\treturn fprintf(summary->fp,\n> > +\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n> > +\t\tname, classname, elapsed);\n> > +}\n> > +\n> > +static int clar_summary_failure(struct clar_summary *summary,\n> > +    const char *type, const char *message, const char *desc)\n> > +{\n> > +\treturn fprintf(summary->fp,\n> > +\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n> > +\t    type, message, desc);\n> > +}\n> > +\n> > +static int clar_summary_skipped(struct clar_summary *summary)\n> > +{\n> > +\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n> > +}\n> > +\n> > +struct clar_summary *clar_summary_init(const char *filename)\n> > +{\n> > +\tstruct clar_summary *summary;\n> > +\tFILE *fp;\n> > +\n> > +\tif ((fp = fopen(filename, \"w\")) == NULL) {\n> > +\t\tperror(\"fopen\");\n> > +\t\treturn NULL;\n> > +\t}\n> > +\n> > +\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n> > +\t\tperror(\"malloc\");\n> > +\t\tfclose(fp);\n> > +\t\treturn NULL;\n> > +\t}\n> > +\n> > +\tsummary->filename = filename;\n> > +\tsummary->fp = fp;\n> > +\n> > +\treturn summary;\n> > +}\n> > +\n> > +int clar_summary_shutdown(struct clar_summary *summary)\n> > +{\n> > +\tstruct clar_report *report;\n> > +\tconst char *last_suite = NULL;\n> > +\n> > +\tif (clar_summary_testsuites(summary) < 0)\n> > +\t\tgoto on_error;\n> > +\n> > +\treport = _clar.reports;\n> > +\twhile (report != NULL) {\n> > +\t\tstruct clar_error *error = report->errors;\n> > +\n> > +\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) !=\n> > 0) {\n> > +\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n> > +\t\t\t    report->start, _clar.tests_ran,\n> > _clar.total_errors, 0) < 0)\n> > +\t\t\t\tgoto on_error;\n> > +\t\t}\n> > +\n> > +\t\tlast_suite = report->suite;\n> > +\n> > +\t\tclar_summary_testcase(summary, report->test, report->suite,\n> > report->elapsed);\n> > +\n> > +\t\twhile (error != NULL) {\n> > +\t\t\tif (clar_summary_failure(summary, \"assert\",\n> > +\t\t\t    error->error_msg, error->description) < 0)\n> > +\t\t\t\tgoto on_error;\n> > +\n> > +\t\t\terror = error->next;\n> > +\t\t}\n> > +\n> > +\t\tif (report->status == CL_TEST_SKIP)\n> > +\t\t\tclar_summary_skipped(summary);\n> > +\n> > +\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n> > +\t\t\tgoto on_error;\n> > +\n> > +\t\treport = report->next;\n> > +\n> > +\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n> > +\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) <\n> > 0)\n> > +\t\t\t\tgoto on_error;\n> > +\t\t}\n> > +\t}\n> > +\n> > +\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n> > +\t    fclose(summary->fp) != 0)\n> > +\t\tgoto on_error;\n> > +\n> > +\tprintf(\"written summary file to %s\\n\", summary->filename);\n> > +\n> > +\tfree(summary);\n> > +\treturn 0;\n> > +\n> > +on_error:\n> > +\tfclose(summary->fp);\n> > +\tfree(summary);\n> > +\treturn -1;\n> > +}\n> > diff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\n> > new file mode 100755\n> > index 0000000000..80996ac3e7\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/generate.py\n> > @@ -0,0 +1,266 @@\n> > +#!/usr/bin/env python\n> > +#\n> > +# Copyright (c) Vicent Marti. All rights reserved.\n> > +#\n> > +# This file is part of clar, distributed under the ISC license.\n> > +# For full terms see the included COPYING file.\n> > +#\n> > +\n> > +from __future__ import with_statement\n> > +from string import Template\n> > +import re, fnmatch, os, sys, codecs, pickle\n> > +\n> > +class Module(object):\n> > +    class Template(object):\n> > +        def __init__(self, module):\n> > +            self.module = module\n> > +\n> > +        def _render_callback(self, cb):\n> > +            if not cb:\n> > +                return '    { NULL, NULL }'\n> > +            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n> > +\n> > +    class DeclarationTemplate(Template):\n> > +        def render(self):\n> > +            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in\n> > self.module.callbacks) + \"\\n\"\n> > +\n> > +            for initializer in self.module.initializers:\n> > +                out += \"extern %s;\\n\" % initializer['declaration']\n> > +\n> > +            if self.module.cleanup:\n> > +                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n> > +\n> > +            return out\n> > +\n> > +    class CallbacksTemplate(Template):\n> > +        def render(self):\n> > +            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" %\n> > self.module.name\n> > +            out += \",\\n\".join(self._render_callback(cb) for cb in\n> > self.module.callbacks)\n> > +            out += \"\\n};\\n\"\n> > +            return out\n> > +\n> > +    class InfoTemplate(Template):\n> > +        def render(self):\n> > +            templates = []\n> > +\n> > +            initializers = self.module.initializers\n> > +            if len(initializers) == 0:\n> > +                initializers = [ None ]\n> > +\n> > +            for initializer in initializers:\n> > +                name = self.module.clean_name()\n> > +                if initializer and\n> > initializer['short_name'].startswith('initialize_'):\n> > +                    variant =\n> > initializer['short_name'][len('initialize_'):]\n> > +                    name += \" (%s)\" % variant.replace('_', ' ')\n> > +\n> > +                template = Template(\n> > +            r\"\"\"\n> > +    {\n> > +        \"${clean_name}\",\n> > +    ${initialize},\n> > +    ${cleanup},\n> > +        ${cb_ptr}, ${cb_count}, ${enabled}\n> > +    }\"\"\"\n> > +                ).substitute(\n> > +                    clean_name = name,\n> > +                    initialize = self._render_callback(initializer),\n> > +                    cleanup = self._render_callback(self.module.cleanup),\n> > +                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n> > +                    cb_count = len(self.module.callbacks),\n> > +                    enabled = int(self.module.enabled)\n> > +                )\n> > +                templates.append(template)\n> > +\n> > +            return ','.join(templates)\n> > +\n> > +    def __init__(self, name):\n> > +        self.name = name\n> > +\n> > +        self.mtime = None\n> > +        self.enabled = True\n> > +        self.modified = False\n> > +\n> > +    def clean_name(self):\n> > +        return self.name.replace(\"_\", \"::\")\n> > +\n> > +    def _skip_comments(self, text):\n> > +        SKIP_COMMENTS_REGEX = re.compile(\n> > +            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n> > +            re.DOTALL | re.MULTILINE)\n> > +\n> > +        def _replacer(match):\n> > +            s = match.group(0)\n> > +            return \"\" if s.startswith('/') else s\n> > +\n> > +        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n> > +\n> > +    def parse(self, contents):\n> > +        TEST_FUNC_REGEX =\n> > r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n> > +\n> > +        contents = self._skip_comments(contents)\n> > +        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n> > +\n> > +        self.callbacks = []\n> > +        self.initializers = []\n> > +        self.cleanup = None\n> > +\n> > +        for (declaration, symbol, short_name) in regex.findall(contents):\n> > +            data = {\n> > +                \"short_name\" : short_name,\n> > +                \"declaration\" : declaration,\n> > +                \"symbol\" : symbol\n> > +            }\n> > +\n> > +            if short_name.startswith('initialize'):\n> > +                self.initializers.append(data)\n> > +            elif short_name == 'cleanup':\n> > +                self.cleanup = data\n> > +            else:\n> > +                self.callbacks.append(data)\n> > +\n> > +        return self.callbacks != []\n> > +\n> > +    def refresh(self, path):\n> > +        self.modified = False\n> > +\n> > +        try:\n> > +            st = os.stat(path)\n> > +\n> > +            # Not modified\n> > +            if st.st_mtime == self.mtime:\n> > +                return True\n> > +\n> > +            self.modified = True\n> > +            self.mtime = st.st_mtime\n> > +\n> > +            with codecs.open(path, encoding='utf-8') as fp:\n> > +                raw_content = fp.read()\n> > +\n> > +        except IOError:\n> > +            return False\n> > +\n> > +        return self.parse(raw_content)\n> > +\n> > +class TestSuite(object):\n> > +\n> > +    def __init__(self, path, output):\n> > +        self.path = path\n> > +        self.output = output\n> > +\n> > +    def should_generate(self, path):\n> > +        if not os.path.isfile(path):\n> > +            return True\n> > +\n> > +        if any(module.modified for module in self.modules.values()):\n> > +            return True\n> > +\n> > +        return False\n> > +\n> > +    def find_modules(self):\n> > +        modules = []\n> > +        for root, _, files in os.walk(self.path):\n> > +            module_root = root[len(self.path):]\n> > +            module_root = [c for c in module_root.split(os.sep) if c]\n> > +\n> > +            tests_in_module = fnmatch.filter(files, \"*.c\")\n> > +\n> > +            for test_file in tests_in_module:\n> > +                full_path = os.path.join(root, test_file)\n> > +                module_name = \"_\".join(module_root +\n> > [test_file[:-2]]).replace(\"-\", \"_\")\n> > +\n> > +                modules.append((full_path, module_name))\n> > +\n> > +        return modules\n> > +\n> > +    def load_cache(self):\n> > +        path = os.path.join(self.output, '.clarcache')\n> > +        cache = {}\n> > +\n> > +        try:\n> > +            fp = open(path, 'rb')\n> > +            cache = pickle.load(fp)\n> > +            fp.close()\n> > +        except (IOError, ValueError):\n> > +            pass\n> > +\n> > +        return cache\n> > +\n> > +    def save_cache(self):\n> > +        path = os.path.join(self.output, '.clarcache')\n> > +        with open(path, 'wb') as cache:\n> > +            pickle.dump(self.modules, cache)\n> > +\n> > +    def load(self, force = False):\n> > +        module_data = self.find_modules()\n> > +        self.modules = {} if force else self.load_cache()\n> > +\n> > +        for path, name in module_data:\n> > +            if name not in self.modules:\n> > +                self.modules[name] = Module(name)\n> > +\n> > +            if not self.modules[name].refresh(path):\n> > +                del self.modules[name]\n> > +\n> > +    def disable(self, excluded):\n> > +        for exclude in excluded:\n> > +            for module in self.modules.values():\n> > +                name = module.clean_name()\n> > +                if name.startswith(exclude):\n> > +                    module.enabled = False\n> > +                    module.modified = True\n> > +\n> > +    def suite_count(self):\n> > +        return sum(max(1, len(m.initializers)) for m in\n> > self.modules.values())\n> > +\n> > +    def callback_count(self):\n> > +        return sum(len(module.callbacks) for module in\n> > self.modules.values())\n> > +\n> > +    def write(self):\n> > +        output = os.path.join(self.output, 'clar.suite')\n> > +\n> > +        if not self.should_generate(output):\n> > +            return False\n> > +\n> > +        with open(output, 'w') as data:\n> > +            modules = sorted(self.modules.values(), key=lambda module:\n> > module.name)\n> > +\n> > +            for module in modules:\n> > +                t = Module.DeclarationTemplate(module)\n> > +                data.write(t.render())\n> > +\n> > +            for module in modules:\n> > +                t = Module.CallbacksTemplate(module)\n> > +                data.write(t.render())\n> > +\n> > +            suites = \"static struct clar_suite _clar_suites[] = {\" +\n> > ','.join(\n> > +                Module.InfoTemplate(module).render() for module in modules\n> > +            ) + \"\\n};\\n\"\n> > +\n> > +            data.write(suites)\n> > +\n> > +            data.write(\"static const size_t _clar_suite_count = %d;\\n\" %\n> > self.suite_count())\n> > +            data.write(\"static const size_t _clar_callback_count = %d;\\n\" %\n> > self.callback_count())\n> > +\n> > +        self.save_cache()\n> > +        return True\n> > +\n> > +if __name__ == '__main__':\n> > +    from optparse import OptionParser\n> > +\n> > +    parser = OptionParser()\n> > +    parser.add_option('-f', '--force', action=\"store_true\", dest='force',\n> > default=False)\n> > +    parser.add_option('-x', '--exclude', dest='excluded', action='append',\n> > default=[])\n> > +    parser.add_option('-o', '--output', dest='output')\n> > +\n> > +    options, args = parser.parse_args()\n> > +    if len(args) > 1:\n> > +        print(\"More than one path given\")\n> > +        sys.exit(1)\n> > +\n> > +    path = args.pop() if args else '.'\n> > +    output = options.output or path\n> > +    suite = TestSuite(path, output)\n> > +    suite.load(options.force)\n> > +    suite.disable(options.excluded)\n> > +    if suite.write():\n> > +        print(\"Written `clar.suite` (%d tests in %d suites)\" %\n> > (suite.callback_count(), suite.suite_count()))\n> > diff --git a/t/unit-tests/clar/test/.gitignore\n> > b/t/unit-tests/clar/test/.gitignore\n> > new file mode 100644\n> > index 0000000000..a477d0c40c\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/test/.gitignore\n> > @@ -0,0 +1,4 @@\n> > +clar.suite\n> > +.clarcache\n> > +clar_test\n> > +*.o\n> > diff --git a/t/unit-tests/clar/test/Makefile\n> > b/t/unit-tests/clar/test/Makefile\n> > new file mode 100644\n> > index 0000000000..93c6b2ad32\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/test/Makefile\n> > @@ -0,0 +1,39 @@\n> > +#\n> > +# Copyright (c) Vicent Marti. All rights reserved.\n> > +#\n> > +# This file is part of clar, distributed under the ISC license.\n> > +# For full terms see the included COPYING file.\n> > +#\n> > +\n> > +#\n> > +# Set up the path to the clar sources and to the fixtures directory\n> > +#\n> > +# The fixture path needs to be an absolute path so it can be used\n> > +# even after we have chdir'ed into the test directory while testing.\n> > +#\n> > +CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n> > +TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n> > +CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n> > +CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n> > +\n> > +CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n> > +\n> > +.PHONY: clean\n> > +\n> > +# list the objects that go into our test\n> > +objects = main.o sample.o\n> > +\n> > +# build the test executable itself\n> > +clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n> > +\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n> > +\n> > +# test object files depend on clar macros\n> > +$(objects) : $(CLAR_PATH)clar.h\n> > +\n> > +# build the clar.suite file of test metadata\n> > +clar.suite:\n> > +\tpython \"$(CLAR_PATH)generate.py\" .\n> > +\n> > +# remove all generated files\n> > +clean:\n> > +\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\n> > diff --git a/t/unit-tests/clar/test/clar_test.h\n> > b/t/unit-tests/clar/test/clar_test.h\n> > new file mode 100644\n> > index 0000000000..0fcaa639aa\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/test/clar_test.h\n> > @@ -0,0 +1,16 @@\n> > +/*\n> > + * Copyright (c) Vicent Marti. All rights reserved.\n> > + *\n> > + * This file is part of clar, distributed under the ISC license.\n> > + * For full terms see the included COPYING file.\n> > + */\n> > +#ifndef __CLAR_TEST__\n> > +#define __CLAR_TEST__\n> > +\n> > +/* Import the standard clar helper functions */\n> > +#include \"clar.h\"\n> > +\n> > +/* Your custom shared includes / defines here */\n> > +extern int global_test_counter;\n> > +\n> > +#endif\n> > diff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\n> > new file mode 100644\n> > index 0000000000..59e56ad255\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/test/main.c\n> > @@ -0,0 +1,40 @@\n> > +/*\n> > + * Copyright (c) Vicent Marti. All rights reserved.\n> > + *\n> > + * This file is part of clar, distributed under the ISC license.\n> > + * For full terms see the included COPYING file.\n> > + */\n> > +\n> > +#include \"clar_test.h\"\n> > +\n> > +/*\n> > + * Sample main() for clar tests.\n> > + *\n> > + * You should write your own main routine for clar tests that does specific\n> > + * setup and teardown as necessary for your application.  The only required\n> > + * line is the call to `clar_test(argc, argv)`, which will execute the test\n> > + * suite.  If you want to check the return value of the test application,\n> > + * your main() should return the same value returned by clar_test().\n> > + */\n> > +\n> > +int global_test_counter = 0;\n> > +\n> > +#ifdef _WIN32\n> > +int __cdecl main(int argc, char *argv[])\n> > +#else\n> > +int main(int argc, char *argv[])\n> > +#endif\n> > +{\n> > +\tint ret;\n> > +\n> > +\t/* Your custom initialization here */\n> > +\tglobal_test_counter = 0;\n> > +\n> > +\t/* Run the test suite */\n> > +\tret = clar_test(argc, argv);\n> > +\n> > +\t/* Your custom cleanup here */\n> > +\tcl_assert_equal_i(8, global_test_counter);\n> > +\n> > +\treturn ret;\n> > +}\n> > diff --git a/t/unit-tests/clar/test/main.c.sample\n> > b/t/unit-tests/clar/test/main.c.sample\n> > new file mode 100644\n> > index 0000000000..a4d91b72fa\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/test/main.c.sample\n> > @@ -0,0 +1,27 @@\n> > +/*\n> > + * Copyright (c) Vicent Marti. All rights reserved.\n> > + *\n> > + * This file is part of clar, distributed under the ISC license.\n> > + * For full terms see the included COPYING file.\n> > + */\n> > +\n> > +#include \"clar_test.h\"\n> > +\n> > +/*\n> > + * Minimal main() for clar tests.\n> > + *\n> > + * Modify this with any application specific setup or teardown that you\n> > need.\n> > + * The only required line is the call to `clar_test(argc, argv)`, which\n> > will\n> > + * execute the test suite.  If you want to check the return value of the\n> > test\n> > + * application, main() should return the same value returned by\n> > clar_test().\n> > + */\n> > +\n> > +#ifdef _WIN32\n> > +int __cdecl main(int argc, char *argv[])\n> > +#else\n> > +int main(int argc, char *argv[])\n> > +#endif\n> > +{\n> > +\t/* Run the test suite */\n> > +\treturn clar_test(argc, argv);\n> > +}\n> > diff --git a/t/unit-tests/clar/test/resources/test/file\n> > b/t/unit-tests/clar/test/resources/test/file\n> > new file mode 100644\n> > index 0000000000..220f4aa98a\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/test/resources/test/file\n> > @@ -0,0 +1 @@\n> > +File\n> > diff --git a/t/unit-tests/clar/test/sample.c\n> > b/t/unit-tests/clar/test/sample.c\n> > new file mode 100644\n> > index 0000000000..faa1209262\n> > --- /dev/null\n> > +++ b/t/unit-tests/clar/test/sample.c\n> > @@ -0,0 +1,84 @@\n> > +#include \"clar_test.h\"\n> > +#include <sys/stat.h>\n> > +\n> > +static int file_size(const char *filename)\n> > +{\n> > +\tstruct stat st;\n> > +\n> > +\tif (stat(filename, &st) == 0)\n> > +\t\treturn (int)st.st_size;\n> > +\treturn -1;\n> > +}\n> > +\n> > +void test_sample__initialize(void)\n> > +{\n> > +\tglobal_test_counter++;\n> > +}\n> > +\n> > +void test_sample__cleanup(void)\n> > +{\n> > +\tcl_fixture_cleanup(\"test\");\n> > +\n> > +\tcl_assert(file_size(\"test/file\") == -1);\n> > +}\n> > +\n> > +void test_sample__1(void)\n> > +{\n> > +\tcl_assert(1);\n> > +\tcl_must_pass(0);  /* 0 == success */\n> > +\tcl_must_fail(-1); /* <0 == failure */\n> > +\tcl_must_pass(-1); /* demonstrate a failing call */\n> > +}\n> > +\n> > +void test_sample__2(void)\n> > +{\n> > +\tcl_fixture_sandbox(\"test\");\n> > +\n> > +\tcl_assert(file_size(\"test/nonexistent\") == -1);\n> > +\tcl_assert(file_size(\"test/file\") > 0);\n> > +\tcl_assert(100 == 101);\n> > +}\n> > +\n> > +void test_sample__strings(void)\n> > +{\n> > +\tconst char *actual = \"expected\";\n> > +\tcl_assert_equal_s(\"expected\", actual);\n> > +\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n> > +\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n> > +}\n> > +\n> > +void test_sample__strings_with_length(void)\n> > +{\n> > +\tconst char *actual = \"expected\";\n> > +\tcl_assert_equal_strn(\"expected_\", actual, 8);\n> > +\tcl_assert_equal_strn(\"exactly\", actual, 2);\n> > +\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n> > +\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n> > +}\n> > +\n> > +void test_sample__int(void)\n> > +{\n> > +\tint value = 100;\n> > +\tcl_assert_equal_i(100, value);\n> > +\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n> > +}\n> > +\n> > +void test_sample__int_fmt(void)\n> > +{\n> > +\tint value = 100;\n> > +\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n> > +}\n> > +\n> > +void test_sample__bool(void)\n> > +{\n> > +\tint value = 100;\n> > +\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n> > +\tcl_assert_equal_b(0, value);\n> > +}\n> > +\n> > +void test_sample__ptr(void)\n> > +{\n> > +\tconst char *actual = \"expected\";\n> > +\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n> > +\tcl_assert_equal_p(&actual, actual);\n> > +}\n>\n"},{"id":"501763","messageId":"435e3840-ee46-4ca3-a525-747848da9930@gmail.com","threadId":"61874","inReplyTo":"1710e9f9ff75cee76be6d5c1311344c6564e3801.1724159966.git.ps@pks.im","subject":"Re: [PATCH v6 02/13] t: import the clar unit testing framework","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-28T13:16:52Z","receivedAt":"2024-08-28T13:16:55Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 20/08/2024 15:02, Patrick Steinhardt wrote:\n> Import the clar unit testing framework at commit 1516124 (Merge pull\n> request #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\n> will be wired up in subsequent commits.\n\nI would be good to explain why we're doing this so we have a record in \nthe project history. A summary of the advantages and disadvantages of \nusing clar vs our current unit test framework would be helpful as well.\n\n > diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\n > [...]\n > +static void\n > +clar_parse_args(int argc, char **argv)\n > +{\n > [...]\n> +\t\tcase 'q':\n> +\t\t\t_clar.report_errors_only = 1;\n> +\t\t\tbreak;\n\nI think this option is incompatible with TAP output as when TAP output \nis selected the first error is printed as part of clar_print_ontest() \nand clar_print_errors() is a no-op. We should error out if it is given \nwith '-t'\n  > +void clar__fail(\n> +\tconst char *file,\n> +\tconst char *function,\n> +\tsize_t line,\n> +\tconst char *error_msg,\n> +\tconst char *description,\n> +\tint should_abort)\n> +{\n> +\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n\nclar seems to take an inconsistent approach to memory allocation errors. \nHere \"error\" is de-referenced without checking if it is NULL and yet in \nother places there are checks for NULL after calling calloc() or strdup().\n\n> +void clar__assert_equal(\n> +\tconst char *file,\n> +\tconst char *function,\n> +\tsize_t line,\n> +\tconst char *err,\n> +\tint should_abort,\n> +\tconst char *fmt,\n> +\t...)\n> +{\n> +\tva_list args;\n> +\tchar buf[4096];\n> +\tint is_equal = 1;\n> +\n> +\tva_start(args, fmt);\n> +\n> +\tif (!strcmp(\"%s\", fmt)) {\n> +\t\tconst char *s1 = va_arg(args, const char *);\n> +\t\tconst char *s2 = va_arg(args, const char *);\n> +\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n> +\n> +\t\tif (!is_equal) {\n> +\t\t\tif (s1 && s2) {\n> +\t\t\t\tint pos;\n> +\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n> +\t\t\t\t\t/* find differing byte offset */;\n> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n> +\t\t\t\t\ts1, s2, pos);\n\ns1 and s2 are passed to snprintf without checking if they are NULL. As \n'snprintf(buf, sizeof(buf), \"%s\", NULL)' is undefined we should check \nfor NULL here. The use of variadic arguments means there is no type \nchecking so if one is checking the contents of an strbuf and writes\n\n     cl_assert_equal_s(buf, \"expect\");\n\ninstead of\n\n     cl_assert_equal_s(buf->buf, \"expect\");\n\nthe compiler does not warn you. This is a regression compared to \ncheck_str(). We can address this by having cl_assert_equal_s() wrap a \nfunction that takes two strings. Another regression is that if the \nstring contains control characters they are printed verbatim.\n\n> diff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\n > [...]\n> +#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n\nThe (int) casts here mean that we'll silently truncate longer arguments \nand suppress any compiler warnings about comparing incompatible types.\n\n > diff --git a/t/unit-tests/clar/clar/print.h \nb/t/unit-tests/clar/clar/print.h\n > [...]\n> +static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n> +{\n> +\tconst struct clar_error *error = _clar.last_report->errors;\n> +\n> +\t(void)test_name;\n> +\t(void)test_number;\n> +\n> +\tswitch(status) {\n> +\tcase CL_TEST_OK:\n> +\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n> +\t\tbreak;\n> +\tcase CL_TEST_FAILURE:\n> +\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n> +\n> +\t\tprintf(\"    ---\\n\");\n> +\t\tprintf(\"    reason: |\\n\");\n> +\t\tprintf(\"      %s\\n\", error->error_msg);\n> +\n> +\t\tif (error->description)\n> +\t\t\tprintf(\"      %s\\n\", error->description);\n> +\n> +\t\tprintf(\"    at:\\n\");\n> +\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n> +\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n> +\t\tprintf(\"      function: '%s'\\n\", error->function);\n> +\t\tprintf(\"    ---\\n\");\n\nIf a test calls cl_warning() only the message from the first call to \nthat function is printed and any message from later calls to \ncl_warning() or clar__fail() is suppressed when TAP output is selected.\n\nBest Wishes\n\nPhillip\n"},{"id":"501764","messageId":"c6f13f6b-7899-4bbd-986a-9bb1649b214f@gmail.com","threadId":"61874","inReplyTo":"b3b8df048725c25b14860513b7950b158a6990ea.1724159966.git.ps@pks.im","subject":"Re: [PATCH v6 11/13] t/unit-tests: convert strvec tests to use clar","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-28T13:17:05Z","receivedAt":"2024-08-28T13:17:07Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 20/08/2024 15:02, Patrick Steinhardt wrote:\n> Convert the strvec tests to use the new clar unit testing framework.\n> This is a first test balloon that demonstrates how the testing infra for\n> clar-based tests looks like.\n> \n> The tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\n> running that binary, it generates TAP output:\n\nIt would be interesting to see a comparison between the current \nframework and clar of the output from a failing test - the TAP output \nfor passing tests is pretty much the same regardless of the framework used.\n\n>      # ./t/unit-tests/bin/unit-tests\n>      TAP version 13\n>      # start of suite 1: strvec\n>      ok 1 - strvec::init\n> [...] \n> The binary also supports some parameters that allow us to run only a\n> subset of unit tests or alter the output:\n> \n>      $ ./t/unit-tests/bin/unit-tests -h\n>      Usage: ./t/unit-tests/bin/unit-tests [options]\n> \n>      Options:\n>        -sname        Run only the suite with `name` (can go to individual test name)\n>        -iname        Include the suite with `name`\n>        -xname        Exclude the suite with `name`\n>        -v            Increase verbosity (show suite names)\n\nThe output above seems to include the suite name - are we running the \ntests with '-v' from our Makefile?\n\n>        -q            Only report tests that had an error\n\nThis option is incompatible with TAP output. As we force TAP output we \nshould find a way to stop displaying this help.\n\n>        -Q            Quit as soon as a test fails\n>        -t            Display results in tap format\n\nWe force TAP output by adding '-t' to argv in main() so this line is not \nvery helpful\n\n>        -l            Print suite names\n>        -r[filename]  Write summary file (to the optional filename)\n\n> diff --git a/t/unit-tests/strvec.c b/t/unit-tests/strvec.c\n> [..]\n> +#define check_strvec(vec, ...) \\\n> +\tdo { \\\n> +\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n> +\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n\nAs there are a lot occurrences of ARRAY_SIZE(expect) it is probably \nworth adding\n\n\tsize_t expect_len = ARRAY_SIZE(expect);\n\nabove.\n\n> +\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n> +\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n> +\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n\nThe conversion here loses the values of nr and alloc which is a shame as \nthey would be useful when debugging a test failure.\n\n> +\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n> +\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n\nThe original test also printed the array index of the failing check. As \nthe elements of the test vectors all seem to be unique that is less of a \nworry than if we had tests with repeating elements.\n\n> +\t} while (0)\n> +\n> +void test_strvec__init(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n\nIf we're rewriting the tests perhaps we can take the opportunity to add \na blank line to each one after the variable declarations in accordance \nwith our coding guidelines.\n\nIt might be a good opportunity to show the set-up and tear-down \nfacilities in clar as well instead of repeating the initialization in \neach test.\n\nBest Wishes\n\nPhillip\n\n> +\tcl_assert_equal_p(vec.v, empty_strvec);\n> +\tcl_assert_equal_i(vec.nr, 0);\n> +\tcl_assert_equal_i(vec.alloc, 0);\n> +}\n> +\n> +void test_strvec__dynamic_init(void)\n> +{\n> +\tstruct strvec vec;\n> +\tstrvec_init(&vec);\n> +\tcl_assert_equal_p(vec.v, empty_strvec);\n> +\tcl_assert_equal_i(vec.nr, 0);\n> +\tcl_assert_equal_i(vec.alloc, 0);\n> +}\n> +\n> +void test_strvec__clear(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_push(&vec, \"foo\");\n> +\tstrvec_clear(&vec);\n> +\tcl_assert_equal_p(vec.v, empty_strvec);\n> +\tcl_assert_equal_i(vec.nr, 0);\n> +\tcl_assert_equal_i(vec.alloc, 0);\n> +}\n> +\n> +void test_strvec__push(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\n> +\tstrvec_push(&vec, \"foo\");\n> +\tcheck_strvec(&vec, \"foo\", NULL);\n> +\n> +\tstrvec_push(&vec, \"bar\");\n> +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n> +\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__pushf(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushf(&vec, \"foo: %d\", 1);\n> +\tcheck_strvec(&vec, \"foo: 1\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__pushl(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__pushv(void)\n> +{\n> +\tconst char *strings[] = {\n> +\t\t\"foo\", \"bar\", \"baz\", NULL,\n> +\t};\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\n> +\tstrvec_pushv(&vec, strings);\n> +\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__replace_at_head(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_replace(&vec, 0, \"replaced\");\n> +\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__replace_at_tail(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_replace(&vec, 2, \"replaced\");\n> +\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__replace_in_between(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_replace(&vec, 1, \"replaced\");\n> +\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__replace_with_substring(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", NULL);\n> +\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n> +\tcheck_strvec(&vec, \"oo\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__remove_at_head(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_remove(&vec, 0);\n> +\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__remove_at_tail(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_remove(&vec, 2);\n> +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__remove_in_between(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_remove(&vec, 1);\n> +\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__pop_empty_array(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pop(&vec);\n> +\tcheck_strvec(&vec, NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__pop_non_empty_array(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_pop(&vec);\n> +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__split_empty_string(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_split(&vec, \"\");\n> +\tcheck_strvec(&vec, NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__split_single_item(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_split(&vec, \"foo\");\n> +\tcheck_strvec(&vec, \"foo\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__split_multiple_items(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_split(&vec, \"foo bar baz\");\n> +\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__split_whitespace_only(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_split(&vec, \" \\t\\n\");\n> +\tcheck_strvec(&vec, NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__split_multiple_consecutive_whitespaces(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tstrvec_split(&vec, \"foo\\n\\t bar\");\n> +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n> +\tstrvec_clear(&vec);\n> +}\n> +\n> +void test_strvec__detach(void)\n> +{\n> +\tstruct strvec vec = STRVEC_INIT;\n> +\tconst char **detached;\n> +\n> +\tstrvec_push(&vec, \"foo\");\n> +\n> +\tdetached = strvec_detach(&vec);\n> +\tcl_assert_equal_s(detached[0], \"foo\");\n> +\tcl_assert_equal_p(detached[1], NULL);\n> +\n> +\tcl_assert_equal_p(vec.v, empty_strvec);\n> +\tcl_assert_equal_i(vec.nr, 0);\n> +\tcl_assert_equal_i(vec.alloc, 0);\n> +\n> +\tfree((char *) detached[0]);\n> +\tfree(detached);\n> +}\n> diff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\n> deleted file mode 100644\n> index c4bac8fc91b..00000000000\n> --- a/t/unit-tests/t-strvec.c\n> +++ /dev/null\n> @@ -1,211 +0,0 @@\n> -#include \"test-lib.h\"\n> -#include \"strbuf.h\"\n> -#include \"strvec.h\"\n> -\n> -#define check_strvec(vec, ...) \\\n> -\tdo { \\\n> -\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n> -\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n> -\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n> -\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n> -\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n> -\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n> -\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n> -\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n> -\t\t\t\t\t\t (uintmax_t)i); \\\n> -\t\t\t\t\tbreak; \\\n> -\t\t\t\t} \\\n> -\t\t\t} \\\n> -\t\t} \\\n> -\t} while (0)\n> -\n> -int cmd_main(int argc, const char **argv)\n> -{\n> -\tif_test (\"static initialization\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tcheck_pointer_eq(vec.v, empty_strvec);\n> -\t\tcheck_uint(vec.nr, ==, 0);\n> -\t\tcheck_uint(vec.alloc, ==, 0);\n> -\t}\n> -\n> -\tif_test (\"dynamic initialization\") {\n> -\t\tstruct strvec vec;\n> -\t\tstrvec_init(&vec);\n> -\t\tcheck_pointer_eq(vec.v, empty_strvec);\n> -\t\tcheck_uint(vec.nr, ==, 0);\n> -\t\tcheck_uint(vec.alloc, ==, 0);\n> -\t}\n> -\n> -\tif_test (\"clear\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_push(&vec, \"foo\");\n> -\t\tstrvec_clear(&vec);\n> -\t\tcheck_pointer_eq(vec.v, empty_strvec);\n> -\t\tcheck_uint(vec.nr, ==, 0);\n> -\t\tcheck_uint(vec.alloc, ==, 0);\n> -\t}\n> -\n> -\tif_test (\"push\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\n> -\t\tstrvec_push(&vec, \"foo\");\n> -\t\tcheck_strvec(&vec, \"foo\", NULL);\n> -\n> -\t\tstrvec_push(&vec, \"bar\");\n> -\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n> -\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"pushf\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushf(&vec, \"foo: %d\", 1);\n> -\t\tcheck_strvec(&vec, \"foo: 1\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"pushl\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"pushv\") {\n> -\t\tconst char *strings[] = {\n> -\t\t\t\"foo\", \"bar\", \"baz\", NULL,\n> -\t\t};\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\n> -\t\tstrvec_pushv(&vec, strings);\n> -\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"replace at head\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_replace(&vec, 0, \"replaced\");\n> -\t\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"replace at tail\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_replace(&vec, 2, \"replaced\");\n> -\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"replace in between\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_replace(&vec, 1, \"replaced\");\n> -\t\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"replace with substring\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", NULL);\n> -\t\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n> -\t\tcheck_strvec(&vec, \"oo\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"remove at head\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_remove(&vec, 0);\n> -\t\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"remove at tail\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_remove(&vec, 2);\n> -\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"remove in between\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_remove(&vec, 1);\n> -\t\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"pop with empty array\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pop(&vec);\n> -\t\tcheck_strvec(&vec, NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"pop with non-empty array\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_pop(&vec);\n> -\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"split empty string\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_split(&vec, \"\");\n> -\t\tcheck_strvec(&vec, NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"split single item\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_split(&vec, \"foo\");\n> -\t\tcheck_strvec(&vec, \"foo\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"split multiple items\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_split(&vec, \"foo bar baz\");\n> -\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"split whitespace only\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_split(&vec, \" \\t\\n\");\n> -\t\tcheck_strvec(&vec, NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"split multiple consecutive whitespaces\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tstrvec_split(&vec, \"foo\\n\\t bar\");\n> -\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n> -\t\tstrvec_clear(&vec);\n> -\t}\n> -\n> -\tif_test (\"detach\") {\n> -\t\tstruct strvec vec = STRVEC_INIT;\n> -\t\tconst char **detached;\n> -\n> -\t\tstrvec_push(&vec, \"foo\");\n> -\n> -\t\tdetached = strvec_detach(&vec);\n> -\t\tcheck_str(detached[0], \"foo\");\n> -\t\tcheck_pointer_eq(detached[1], NULL);\n> -\n> -\t\tcheck_pointer_eq(vec.v, empty_strvec);\n> -\t\tcheck_uint(vec.nr, ==, 0);\n> -\t\tcheck_uint(vec.alloc, ==, 0);\n> -\n> -\t\tfree((char *) detached[0]);\n> -\t\tfree(detached);\n> -\t}\n> -\n> -\treturn test_done();\n> -}\n"},{"id":"501765","messageId":"2dbdb173-9e31-4dbc-a65a-dd952ec1c213@gmail.com","threadId":"61874","inReplyTo":"1ac2e48a7f2d41d60ff56890d8d87125f30c2f76.1724159966.git.ps@pks.im","subject":"Re: [PATCH v6 12/13] t/unit-tests: convert ctype tests to use clar","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-28T13:18:02Z","receivedAt":"2024-08-28T13:18:05Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 20/08/2024 15:02, Patrick Steinhardt wrote:\n> Convert the ctype tests to use the new clar unit testing framework.\n> Introduce a new function `cl_failf()` \n\nThis is a nice addition which somewhat mitigates the lack of an \nequivalent to test_msg() that adds addition context messages to test \nfailures.\n\n> that allows us to print a\n> formatted error message, which we can use to point out which of the\n> characters was classified incorrectly. This results in output like this\n> on failure:\n> \n>      # start of suite 1: ctype\n>      ok 1 - ctype::isspace\n>      not ok 2 - ctype::isdigit\n>          ---\n>          reason: |\n>            Test failed.\n\n\"Test failed.\" is not the reason for the test failure\n\n>            0x61 is classified incorrectly\n>          at:\n>            file: 't/unit-tests/ctype.c'\n>            line: 38\n>            function: 'test_ctype__isdigit'\n>          ---\n\nThis is rather verbose compared to the current framework\n\n# check \"isdigit(i) == !!memchr(\"123456789\", i, len)\" failed at \nt/unit-tests/t-ctype.c:36\n#    left: 1\n#   right: 0\n#       i: 0x30\nnot ok 2 - isdigit works\n\nThe current tests also shows which characters are expected to return \ntrue and distinguishes between the two possible failure modes which are \n(a) misclassification and (b) returning a non-zero integer other than \n'1' as \"true\". The new test output does not allow the person running the \ntest to distinguish between these two failure modes.\n\n> diff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\n> index 66ec2387cc6..4c461defe16 100644\n> --- a/t/unit-tests/unit-test.h\n> +++ b/t/unit-tests/unit-test.h\n> @@ -1,3 +1,10 @@\n>   #include \"git-compat-util.h\"\n>   #include \"clar/clar.h\"\n>   #include \"clar-decls.h\"\n> +#include \"strbuf.h\"\n> +\n> +#define cl_failf(fmt, ...) do { \\\n> +\tchar *desc = xstrfmt(fmt, __VA_ARGS__); \\\n\nIn our current framework we avoid relying on the strbuf api and \nfunctions like xstrfmt() that use it as we want to be able to test \nstrbuf.c with the framework. We'd be better with a macro wrapping a new \nfunction that uses a stack allocated buffer and p_snprintf() like \nclar__assert_equal(). That would allow us to upstream this change as well.\n\n> +\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n> +\tfree(desc); \\\n\nThis is leaked on failure due to the use of longjmp.\n\nBest Wishes\n\nPhillip\n\n> +} while (0)\n"},{"id":"501766","messageId":"e9c015a6-7fc2-4783-9ef6-05359adf46cc@gmail.com","threadId":"61874","inReplyTo":"cover.1724159966.git.ps@pks.im","subject":"Re: [PATCH v6 00/13] Introduce clar testing framework","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-28T13:18:49Z","receivedAt":"2024-08-28T13:18:51Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 20/08/2024 15:02, Patrick Steinhardt wrote:\n> Hi,\n> \n> this is another version of my patch series that introduces the clar\n> testing framework for our C unit tests.\n\nI've left some comments on the code, most of them are points I made in \nthe last round that received no response. My main concern is that the \nassertions offered by clar are not as convinent as the check_* macros.\n\nWhat's the plan for converting our current tests if this gets merged? If \nwe were to add wrappers that provide check_int() etc. that would greatly \nsimplify the conversion. I think it would offer a more ergonomic api for \nwriting new tests than the verbose and non-typesafe cl_assert_equal_i() \nand friends.\n\nBest Wishes\n\nPhillip\n\n> Changes compared to v5:\n> \n>    - Rebased on top of `master` to fix some merge conflicts. This series\n>      is now built on bb9c16bd4f (The sixth batch, 2024-08-19).\n> \n>    - Introduce `cl_failf()` as a nice way to print formatted error\n>      messages.\n> \n>    - NULL-terminate the argv array again and move the changes into the\n>      correct patch.\n> \n>    - Most of the changes were done by Dscho to port clar over to Windows,\n>      including CMake build instructions to make this usable with MSVC. I\n>      plan to upstream these changes when this series lands in Git.\n> \n>    - Introduce CMake support, also by Dscho.\n> \n> Thanks for all the feedback, and thanks to Dscho for helping out with\n> porting this to Windows.\n> \n> GitHub pipeline: https://github.com/git/git/actions/runs/10472008177/\n> GitLab pipeline: https://gitlab.com/gitlab-org/git/-/pipelines/1419946971\n> \n> Patrick\n> \n> Johannes Schindelin (4):\n>    clar: avoid compile error with mingw-w64\n>    clar(win32): avoid compile error due to unused `fs_copy()`\n>    clar: stop including `shellapi.h` unnecessarily\n>    clar: add CMake support\n> \n> Patrick Steinhardt (9):\n>    t: do not pass GIT_TEST_OPTS to unit tests with prove\n>    t: import the clar unit testing framework\n>    t/clar: fix compatibility with NonStop\n>    Makefile: fix sparse dependency on GENERATED_H\n>    Makefile: make hdr-check depend on generated headers\n>    Makefile: do not use sparse on third-party sources\n>    Makefile: wire up the clar unit testing framework\n>    t/unit-tests: convert strvec tests to use clar\n>    t/unit-tests: convert ctype tests to use clar\n> \n>   .gitignore                                 |   1 +\n>   Documentation/technical/unit-tests.txt     |   2 +\n>   Makefile                                   |  53 +-\n>   contrib/buildsystems/CMakeLists.txt        |  53 ++\n>   t/Makefile                                 |   4 +-\n>   t/run-test.sh                              |   2 +-\n>   t/unit-tests/.gitignore                    |   2 +\n>   t/unit-tests/clar-generate.awk             |  50 ++\n>   t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n>   t/unit-tests/clar/COPYING                  |  15 +\n>   t/unit-tests/clar/README.md                | 329 ++++++++\n>   t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n>   t/unit-tests/clar/clar.h                   | 173 +++++\n>   t/unit-tests/clar/clar/fixtures.h          |  50 ++\n>   t/unit-tests/clar/clar/fs.h                | 524 +++++++++++++\n>   t/unit-tests/clar/clar/print.h             | 211 ++++++\n>   t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n>   t/unit-tests/clar/clar/summary.h           | 143 ++++\n>   t/unit-tests/clar/generate.py              | 266 +++++++\n>   t/unit-tests/clar/test/.gitignore          |   4 +\n>   t/unit-tests/clar/test/Makefile            |  39 +\n>   t/unit-tests/clar/test/clar_test.h         |  16 +\n>   t/unit-tests/clar/test/main.c              |  40 +\n>   t/unit-tests/clar/test/main.c.sample       |  27 +\n>   t/unit-tests/clar/test/resources/test/file |   1 +\n>   t/unit-tests/clar/test/sample.c            |  84 ++\n>   t/unit-tests/{t-ctype.c => ctype.c}        |  70 +-\n>   t/unit-tests/strvec.c                      | 222 ++++++\n>   t/unit-tests/t-strvec.c                    | 211 ------\n>   t/unit-tests/unit-test.c                   |  18 +\n>   t/unit-tests/unit-test.h                   |  10 +\n>   31 files changed, 3409 insertions(+), 235 deletions(-)\n>   create mode 100644 t/unit-tests/clar-generate.awk\n>   create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n>   create mode 100644 t/unit-tests/clar/COPYING\n>   create mode 100644 t/unit-tests/clar/README.md\n>   create mode 100644 t/unit-tests/clar/clar.c\n>   create mode 100644 t/unit-tests/clar/clar.h\n>   create mode 100644 t/unit-tests/clar/clar/fixtures.h\n>   create mode 100644 t/unit-tests/clar/clar/fs.h\n>   create mode 100644 t/unit-tests/clar/clar/print.h\n>   create mode 100644 t/unit-tests/clar/clar/sandbox.h\n>   create mode 100644 t/unit-tests/clar/clar/summary.h\n>   create mode 100755 t/unit-tests/clar/generate.py\n>   create mode 100644 t/unit-tests/clar/test/.gitignore\n>   create mode 100644 t/unit-tests/clar/test/Makefile\n>   create mode 100644 t/unit-tests/clar/test/clar_test.h\n>   create mode 100644 t/unit-tests/clar/test/main.c\n>   create mode 100644 t/unit-tests/clar/test/main.c.sample\n>   create mode 100644 t/unit-tests/clar/test/resources/test/file\n>   create mode 100644 t/unit-tests/clar/test/sample.c\n>   rename t/unit-tests/{t-ctype.c => ctype.c} (70%)\n>   create mode 100644 t/unit-tests/strvec.c\n>   delete mode 100644 t/unit-tests/t-strvec.c\n>   create mode 100644 t/unit-tests/unit-test.c\n>   create mode 100644 t/unit-tests/unit-test.h\n> \n> Range-diff against v5:\n>   1:  832dc0496fb =  1:  e48a6461137 t: do not pass GIT_TEST_OPTS to unit tests with prove\n>   2:  36906079330 =  2:  1710e9f9ff7 t: import the clar unit testing framework\n>   3:  db53673294e =  3:  5c21aa87aa2 t/clar: fix compatibility with NonStop\n>   -:  ----------- >  4:  06d2bce0d82 clar: avoid compile error with mingw-w64\n>   -:  ----------- >  5:  f88b3421a09 clar(win32): avoid compile error due to unused `fs_copy()`\n>   -:  ----------- >  6:  5fb4c55be33 clar: stop including `shellapi.h` unnecessarily\n>   4:  b6199c88dd7 =  7:  e0dcbd5ca83 Makefile: fix sparse dependency on GENERATED_H\n>   5:  06364b2b722 =  8:  77a03f8df70 Makefile: make hdr-check depend on generated headers\n>   6:  88ea94ce16c =  9:  c91dd7327e3 Makefile: do not use sparse on third-party sources\n>   7:  05bcb5bef6c = 10:  115c15aa9ae Makefile: wire up the clar unit testing framework\n>   8:  8f56b4d6264 ! 11:  b3b8df04872 t/unit-tests: convert strvec tests to use clar\n>      @@ Makefile: THIRD_PARTY_SOURCES += sha1dc/%\n>        UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n>        UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n>        UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n>      -@@ Makefile: UNIT_TEST_PROGRAMS += t-reftable-merged\n>      - UNIT_TEST_PROGRAMS += t-reftable-record\n>      +@@ Makefile: UNIT_TEST_PROGRAMS += t-reftable-record\n>      + UNIT_TEST_PROGRAMS += t-reftable-tree\n>        UNIT_TEST_PROGRAMS += t-strbuf\n>        UNIT_TEST_PROGRAMS += t-strcmp-offset\n>       -UNIT_TEST_PROGRAMS += t-strvec\n>      @@ Makefile: UNIT_TEST_PROGRAMS += t-reftable-merged\n>        UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n>        UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\n>       \n>      - ## t/unit-tests/t-strvec.c => t/unit-tests/strvec.c ##\n>      + ## t/unit-tests/strvec.c (new) ##\n>       @@\n>      --#include \"test-lib.h\"\n>       +#include \"unit-test.h\"\n>      - #include \"strbuf.h\"\n>      - #include \"strvec.h\"\n>      -\n>      - #define check_strvec(vec, ...) \\\n>      - \tdo { \\\n>      - \t\tconst char *expect[] = { __VA_ARGS__ }; \\\n>      --\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n>      --\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n>      --\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n>      --\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n>      --\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n>      --\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n>      --\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n>      --\t\t\t\t\t\t (uintmax_t)i); \\\n>      --\t\t\t\t\tbreak; \\\n>      --\t\t\t\t} \\\n>      --\t\t\t} \\\n>      --\t\t} \\\n>      ++#include \"strbuf.h\"\n>      ++#include \"strvec.h\"\n>      ++\n>      ++#define check_strvec(vec, ...) \\\n>      ++\tdo { \\\n>      ++\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n>       +\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n>       +\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n>       +\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n>       +\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n>       +\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n>       +\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n>      - \t} while (0)\n>      -\n>      --static void t_static_init(void)\n>      ++\t} while (0)\n>      ++\n>       +void test_strvec__init(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      --\tcheck_pointer_eq(vec.v, empty_strvec);\n>      --\tcheck_uint(vec.nr, ==, 0);\n>      --\tcheck_uint(vec.alloc, ==, 0);\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>       +\tcl_assert_equal_p(vec.v, empty_strvec);\n>       +\tcl_assert_equal_i(vec.nr, 0);\n>       +\tcl_assert_equal_i(vec.alloc, 0);\n>      - }\n>      -\n>      --static void t_dynamic_init(void)\n>      ++}\n>      ++\n>       +void test_strvec__dynamic_init(void)\n>      - {\n>      - \tstruct strvec vec;\n>      - \tstrvec_init(&vec);\n>      --\tcheck_pointer_eq(vec.v, empty_strvec);\n>      --\tcheck_uint(vec.nr, ==, 0);\n>      --\tcheck_uint(vec.alloc, ==, 0);\n>      ++{\n>      ++\tstruct strvec vec;\n>      ++\tstrvec_init(&vec);\n>       +\tcl_assert_equal_p(vec.v, empty_strvec);\n>       +\tcl_assert_equal_i(vec.nr, 0);\n>       +\tcl_assert_equal_i(vec.alloc, 0);\n>      - }\n>      -\n>      --static void t_clear(void)\n>      ++}\n>      ++\n>       +void test_strvec__clear(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_push(&vec, \"foo\");\n>      - \tstrvec_clear(&vec);\n>      --\tcheck_pointer_eq(vec.v, empty_strvec);\n>      --\tcheck_uint(vec.nr, ==, 0);\n>      --\tcheck_uint(vec.alloc, ==, 0);\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_push(&vec, \"foo\");\n>      ++\tstrvec_clear(&vec);\n>       +\tcl_assert_equal_p(vec.v, empty_strvec);\n>       +\tcl_assert_equal_i(vec.nr, 0);\n>       +\tcl_assert_equal_i(vec.alloc, 0);\n>      - }\n>      -\n>      --static void t_push(void)\n>      ++}\n>      ++\n>       +void test_strvec__push(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      -\n>      -@@ t/unit-tests/strvec.c: static void t_push(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_pushf(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>      ++\tstrvec_push(&vec, \"foo\");\n>      ++\tcheck_strvec(&vec, \"foo\", NULL);\n>      ++\n>      ++\tstrvec_push(&vec, \"bar\");\n>      ++\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      ++\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__pushf(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushf(&vec, \"foo: %d\", 1);\n>      -@@ t/unit-tests/strvec.c: static void t_pushf(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_pushl(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushf(&vec, \"foo: %d\", 1);\n>      ++\tcheck_strvec(&vec, \"foo: 1\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__pushl(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_pushl(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_pushv(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__pushv(void)\n>      - {\n>      - \tconst char *strings[] = {\n>      - \t\t\"foo\", \"bar\", \"baz\", NULL,\n>      -@@ t/unit-tests/strvec.c: static void t_pushv(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_replace_at_head(void)\n>      ++{\n>      ++\tconst char *strings[] = {\n>      ++\t\t\"foo\", \"bar\", \"baz\", NULL,\n>      ++\t};\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>      ++\tstrvec_pushv(&vec, strings);\n>      ++\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__replace_at_head(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_replace_at_head(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_replace_at_tail(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_replace(&vec, 0, \"replaced\");\n>      ++\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__replace_at_tail(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_replace_at_tail(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_replace_in_between(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_replace(&vec, 2, \"replaced\");\n>      ++\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__replace_in_between(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_replace_in_between(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_replace_with_substring(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_replace(&vec, 1, \"replaced\");\n>      ++\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__replace_with_substring(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_replace_with_substring(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_remove_at_head(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", NULL);\n>      ++\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n>      ++\tcheck_strvec(&vec, \"oo\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__remove_at_head(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_remove_at_head(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_remove_at_tail(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_remove(&vec, 0);\n>      ++\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__remove_at_tail(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_remove_at_tail(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_remove_in_between(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_remove(&vec, 2);\n>      ++\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__remove_in_between(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_remove_in_between(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_pop_empty_array(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_remove(&vec, 1);\n>      ++\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__pop_empty_array(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pop(&vec);\n>      -@@ t/unit-tests/strvec.c: static void t_pop_empty_array(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_pop_non_empty_array(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pop(&vec);\n>      ++\tcheck_strvec(&vec, NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__pop_non_empty_array(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      -@@ t/unit-tests/strvec.c: static void t_pop_non_empty_array(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_split_empty_string(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_pop(&vec);\n>      ++\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__split_empty_string(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_split(&vec, \"\");\n>      -@@ t/unit-tests/strvec.c: static void t_split_empty_string(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_split_single_item(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_split(&vec, \"\");\n>      ++\tcheck_strvec(&vec, NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__split_single_item(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_split(&vec, \"foo\");\n>      -@@ t/unit-tests/strvec.c: static void t_split_single_item(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_split_multiple_items(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_split(&vec, \"foo\");\n>      ++\tcheck_strvec(&vec, \"foo\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__split_multiple_items(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_split(&vec, \"foo bar baz\");\n>      -@@ t/unit-tests/strvec.c: static void t_split_multiple_items(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_split_whitespace_only(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_split(&vec, \"foo bar baz\");\n>      ++\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__split_whitespace_only(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_split(&vec, \" \\t\\n\");\n>      -@@ t/unit-tests/strvec.c: static void t_split_whitespace_only(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_split_multiple_consecutive_whitespaces(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_split(&vec, \" \\t\\n\");\n>      ++\tcheck_strvec(&vec, NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__split_multiple_consecutive_whitespaces(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tstrvec_split(&vec, \"foo\\n\\t bar\");\n>      -@@ t/unit-tests/strvec.c: static void t_split_multiple_consecutive_whitespaces(void)\n>      - \tstrvec_clear(&vec);\n>      - }\n>      -\n>      --static void t_detach(void)\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tstrvec_split(&vec, \"foo\\n\\t bar\");\n>      ++\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      ++\tstrvec_clear(&vec);\n>      ++}\n>      ++\n>       +void test_strvec__detach(void)\n>      - {\n>      - \tstruct strvec vec = STRVEC_INIT;\n>      - \tconst char **detached;\n>      -@@ t/unit-tests/strvec.c: static void t_detach(void)\n>      - \tstrvec_push(&vec, \"foo\");\n>      -\n>      - \tdetached = strvec_detach(&vec);\n>      --\tcheck_str(detached[0], \"foo\");\n>      --\tcheck_pointer_eq(detached[1], NULL);\n>      ++{\n>      ++\tstruct strvec vec = STRVEC_INIT;\n>      ++\tconst char **detached;\n>      ++\n>      ++\tstrvec_push(&vec, \"foo\");\n>      ++\n>      ++\tdetached = strvec_detach(&vec);\n>       +\tcl_assert_equal_s(detached[0], \"foo\");\n>       +\tcl_assert_equal_p(detached[1], NULL);\n>      -\n>      --\tcheck_pointer_eq(vec.v, empty_strvec);\n>      --\tcheck_uint(vec.nr, ==, 0);\n>      --\tcheck_uint(vec.alloc, ==, 0);\n>      ++\n>       +\tcl_assert_equal_p(vec.v, empty_strvec);\n>       +\tcl_assert_equal_i(vec.nr, 0);\n>       +\tcl_assert_equal_i(vec.alloc, 0);\n>      -\n>      - \tfree((char *) detached[0]);\n>      - \tfree(detached);\n>      - }\n>      ++\n>      ++\tfree((char *) detached[0]);\n>      ++\tfree(detached);\n>      ++}\n>      +\n>      + ## t/unit-tests/t-strvec.c (deleted) ##\n>      +@@\n>      +-#include \"test-lib.h\"\n>      +-#include \"strbuf.h\"\n>      +-#include \"strvec.h\"\n>      +-\n>      +-#define check_strvec(vec, ...) \\\n>      +-\tdo { \\\n>      +-\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n>      +-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n>      +-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n>      +-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n>      +-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n>      +-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n>      +-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n>      +-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n>      +-\t\t\t\t\t\t (uintmax_t)i); \\\n>      +-\t\t\t\t\tbreak; \\\n>      +-\t\t\t\t} \\\n>      +-\t\t\t} \\\n>      +-\t\t} \\\n>      +-\t} while (0)\n>       -\n>       -int cmd_main(int argc, const char **argv)\n>       -{\n>      --\tTEST(t_static_init(), \"static initialization\");\n>      --\tTEST(t_dynamic_init(), \"dynamic initialization\");\n>      --\tTEST(t_clear(), \"clear\");\n>      --\tTEST(t_push(), \"push\");\n>      --\tTEST(t_pushf(), \"pushf\");\n>      --\tTEST(t_pushl(), \"pushl\");\n>      --\tTEST(t_pushv(), \"pushv\");\n>      --\tTEST(t_replace_at_head(), \"replace at head\");\n>      --\tTEST(t_replace_in_between(), \"replace in between\");\n>      --\tTEST(t_replace_at_tail(), \"replace at tail\");\n>      --\tTEST(t_replace_with_substring(), \"replace with substring\");\n>      --\tTEST(t_remove_at_head(), \"remove at head\");\n>      --\tTEST(t_remove_in_between(), \"remove in between\");\n>      --\tTEST(t_remove_at_tail(), \"remove at tail\");\n>      --\tTEST(t_pop_empty_array(), \"pop with empty array\");\n>      --\tTEST(t_pop_non_empty_array(), \"pop with non-empty array\");\n>      --\tTEST(t_split_empty_string(), \"split empty string\");\n>      --\tTEST(t_split_single_item(), \"split single item\");\n>      --\tTEST(t_split_multiple_items(), \"split multiple items\");\n>      --\tTEST(t_split_whitespace_only(), \"split whitespace only\");\n>      --\tTEST(t_split_multiple_consecutive_whitespaces(), \"split multiple consecutive whitespaces\");\n>      --\tTEST(t_detach(), \"detach\");\n>      +-\tif_test (\"static initialization\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n>      +-\t\tcheck_uint(vec.nr, ==, 0);\n>      +-\t\tcheck_uint(vec.alloc, ==, 0);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"dynamic initialization\") {\n>      +-\t\tstruct strvec vec;\n>      +-\t\tstrvec_init(&vec);\n>      +-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n>      +-\t\tcheck_uint(vec.nr, ==, 0);\n>      +-\t\tcheck_uint(vec.alloc, ==, 0);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"clear\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_push(&vec, \"foo\");\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n>      +-\t\tcheck_uint(vec.nr, ==, 0);\n>      +-\t\tcheck_uint(vec.alloc, ==, 0);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"push\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\n>      +-\t\tstrvec_push(&vec, \"foo\");\n>      +-\t\tcheck_strvec(&vec, \"foo\", NULL);\n>      +-\n>      +-\t\tstrvec_push(&vec, \"bar\");\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      +-\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"pushf\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushf(&vec, \"foo: %d\", 1);\n>      +-\t\tcheck_strvec(&vec, \"foo: 1\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"pushl\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"pushv\") {\n>      +-\t\tconst char *strings[] = {\n>      +-\t\t\t\"foo\", \"bar\", \"baz\", NULL,\n>      +-\t\t};\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\n>      +-\t\tstrvec_pushv(&vec, strings);\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"replace at head\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_replace(&vec, 0, \"replaced\");\n>      +-\t\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"replace at tail\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_replace(&vec, 2, \"replaced\");\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"replace in between\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_replace(&vec, 1, \"replaced\");\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"replace with substring\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", NULL);\n>      +-\t\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n>      +-\t\tcheck_strvec(&vec, \"oo\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"remove at head\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_remove(&vec, 0);\n>      +-\t\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"remove at tail\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_remove(&vec, 2);\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"remove in between\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_remove(&vec, 1);\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"pop with empty array\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pop(&vec);\n>      +-\t\tcheck_strvec(&vec, NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"pop with non-empty array\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_pop(&vec);\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"split empty string\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_split(&vec, \"\");\n>      +-\t\tcheck_strvec(&vec, NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"split single item\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_split(&vec, \"foo\");\n>      +-\t\tcheck_strvec(&vec, \"foo\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"split multiple items\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_split(&vec, \"foo bar baz\");\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"split whitespace only\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_split(&vec, \" \\t\\n\");\n>      +-\t\tcheck_strvec(&vec, NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"split multiple consecutive whitespaces\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tstrvec_split(&vec, \"foo\\n\\t bar\");\n>      +-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      +-\t\tstrvec_clear(&vec);\n>      +-\t}\n>      +-\n>      +-\tif_test (\"detach\") {\n>      +-\t\tstruct strvec vec = STRVEC_INIT;\n>      +-\t\tconst char **detached;\n>      +-\n>      +-\t\tstrvec_push(&vec, \"foo\");\n>      +-\n>      +-\t\tdetached = strvec_detach(&vec);\n>      +-\t\tcheck_str(detached[0], \"foo\");\n>      +-\t\tcheck_pointer_eq(detached[1], NULL);\n>      +-\n>      +-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n>      +-\t\tcheck_uint(vec.nr, ==, 0);\n>      +-\t\tcheck_uint(vec.alloc, ==, 0);\n>      +-\n>      +-\t\tfree((char *) detached[0]);\n>      +-\t\tfree(detached);\n>      +-\t}\n>      +-\n>       -\treturn test_done();\n>       -}\n>      -\n>      - ## t/unit-tests/unit-test.c ##\n>      -@@ t/unit-tests/unit-test.c: int cmd_main(int argc, const char **argv)\n>      - \tint ret;\n>      -\n>      - \t/* Append the \"-t\" flag such that the tests generate TAP output. */\n>      --\tALLOC_ARRAY(argv_copy, argc + 2);\n>      -+\tALLOC_ARRAY(argv_copy, argc + 1);\n>      - \tCOPY_ARRAY(argv_copy, argv, argc);\n>      - \targv_copy[argc++] = \"-t\";\n>      --\targv_copy[argc] = NULL;\n>      -\n>      - \tret = clar_test(argc, (char **) argv_copy);\n>      -\n>   9:  ca09d19fd51 ! 12:  1ac2e48a7f2 t/unit-tests: convert ctype tests to use clar\n>      @@ Commit message\n>           t/unit-tests: convert ctype tests to use clar\n>       \n>           Convert the ctype tests to use the new clar unit testing framework.\n>      +    Introduce a new function `cl_failf()` that allows us to print a\n>      +    formatted error message, which we can use to point out which of the\n>      +    characters was classified incorrectly. This results in output like this\n>      +    on failure:\n>      +\n>      +        # start of suite 1: ctype\n>      +        ok 1 - ctype::isspace\n>      +        not ok 2 - ctype::isdigit\n>      +            ---\n>      +            reason: |\n>      +              Test failed.\n>      +              0x61 is classified incorrectly\n>      +            at:\n>      +              file: 't/unit-tests/ctype.c'\n>      +              line: 38\n>      +              function: 'test_ctype__isdigit'\n>      +            ---\n>      +        ok 3 - ctype::isalpha\n>      +        ok 4 - ctype::isalnum\n>      +        ok 5 - ctype::is_glob_special\n>      +        ok 6 - ctype::is_regex_special\n>      +        ok 7 - ctype::is_pathspec_magic\n>      +        ok 8 - ctype::isascii\n>      +        ok 9 - ctype::islower\n>      +        ok 10 - ctype::isupper\n>      +        ok 11 - ctype::iscntrl\n>      +        ok 12 - ctype::ispunct\n>      +        ok 13 - ctype::isxdigit\n>      +        ok 14 - ctype::isprint\n>       \n>           Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>       \n>      @@ Makefile: THIRD_PARTY_SOURCES += sha1dc/%\n>       -UNIT_TEST_PROGRAMS += t-ctype\n>        UNIT_TEST_PROGRAMS += t-example-decorate\n>        UNIT_TEST_PROGRAMS += t-hash\n>      - UNIT_TEST_PROGRAMS += t-mem-pool\n>      + UNIT_TEST_PROGRAMS += t-hashmap\n>       \n>        ## t/unit-tests/t-ctype.c => t/unit-tests/ctype.c ##\n>       @@\n>      @@ t/unit-tests/t-ctype.c => t/unit-tests/ctype.c\n>        \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n>        \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n>        \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n>      --\tint skip = test__run_begin(); \\\n>      --\tif (!skip) { \\\n>      +-\tif_test (#class \" works\") { \\\n>       -\t\tfor (int i = 0; i < 256; i++) { \\\n>       -\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n>       -\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n>       -\t\t} \\\n>       -\t\tcheck(!class(EOF)); \\\n>       -\t} \\\n>      --\ttest__run_end(!skip, TEST_LOCATION(), #class \" works\"); \\\n>       +\tfor (int i = 0; i < 256; i++) \\\n>      -+\t\tcl_assert_equal_i(class(i), !!memchr(string, i, len)); \\\n>      ++\t\tif (class(i) != !!memchr(string, i, len)) \\\n>      ++\t\t\tcl_failf(\"0x%02x is classified incorrectly\", i); \\\n>       +\tcl_assert(!class(EOF)); \\\n>        } while (0)\n>        \n>      @@ t/unit-tests/t-ctype.c => t/unit-tests/ctype.c\n>       +{\n>       +\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n>        }\n>      +\n>      + ## t/unit-tests/unit-test.h ##\n>      +@@\n>      + #include \"git-compat-util.h\"\n>      + #include \"clar/clar.h\"\n>      + #include \"clar-decls.h\"\n>      ++#include \"strbuf.h\"\n>      ++\n>      ++#define cl_failf(fmt, ...) do { \\\n>      ++\tchar *desc = xstrfmt(fmt, __VA_ARGS__); \\\n>      ++\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n>      ++\tfree(desc); \\\n>      ++} while (0)\n>   -:  ----------- > 13:  131036c398e clar: add CMake support\n\n"},{"id":"501767","messageId":"2fc9a0ba-df82-41e2-a014-2cd5e911f01b@gmail.com","threadId":"61874","inReplyTo":"845fa867-f339-9190-b9fa-2e81c02adb6b@gmx.de","subject":"Re: [PATCH v5 2/9] t: import the clar unit testing framework","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-28T13:20:47Z","receivedAt":"2024-08-28T13:20:51Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Dscho\n\nOn 23/08/2024 13:16, Johannes Schindelin wrote:\n> Hi Phillip,\n> \n> thank you for looping me in; I meant to answer earlier.\n> \n> I am very, very happy to see that there is a chance to adopt a test\n> framework like `clar`. Therefore, I am very interested in assisting in\n> every which way I can.\n\nThat's great, I like the basic ideas in clar but I think the assertions \ncould use some improvement.\n\nBest Wishes\n\nPhillip\n\n> On Fri, 16 Aug 2024, Phillip Wood wrote:\n> \n>> Johannes - are these format specifiers supported by the version of MSVCRT that\n>> is used by git for windows? c.f.\n>> https://lore.kernel.org/git/nycvar.QRO.7.76.6.2203241456250.388@tvgsbejvaqbjf.bet/\n> \n> The format specifiers _should_ work well both with GCC and Visual C. What\n> did not work well was using `struct stat` with `_stat()` (note that the\n> latter has a leading underscore and the former does not).\n> \n> There were other issues, top of which was the bit tricky part to adapt the\n> `CMakeLists.txt` definition, in particular the part where `clar.suite` is\n> generated. But I managed to squeeze in that work somehow and Patrick was\n> kind enough to already integrate the patches.\n> \n> Ciao,\n> Johannes\n> \n>> On 16/08/2024 08:04, Patrick Steinhardt wrote:\n>>> diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\n>>> [...]\n>>> +#ifdef _WIN32\n>>> [...]\n>>> +#\tifndef PRIuZ\n>>> +#\t\tdefine PRIuZ \"Iu\"\n>>> +#\tendif\n>>> +#\tifndef PRIxZ\n>>> +#\t\tdefine PRIxZ \"Ix\"\n>>> +#\tendif\n>>\n>> Thanks\n>>\n>> Phillip\n>>\n>>> +#\tif defined(_MSC_VER) || defined(__MINGW32__)\n>>> +\ttypedef struct stat STAT_T;\n>>> +#\telse\n>>> +\ttypedef struct _stat STAT_T;\n>>> +#\tendif\n>>> +#else\n>>> +#\tinclude <sys/wait.h> /* waitpid(2) */\n>>> +#\tinclude <unistd.h>\n>>> +#\tdefine _MAIN_CC\n>>> +#\tdefine p_snprintf snprintf\n>>> +#\tifndef PRIuZ\n>>> +#\t\tdefine PRIuZ \"zu\"\n>>> +#\tendif\n>>> +#\tifndef PRIxZ\n>>> +#\t\tdefine PRIxZ \"zx\"\n>>> +#\tendif\n>>> +\ttypedef struct stat STAT_T;\n>>> +#endif\n>>> +\n>>> +#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n>>> +\n>>> +#include \"clar.h\"\n>>> +\n>>> +static void fs_rm(const char *_source);\n>>> +static void fs_copy(const char *_source, const char *dest);\n>>> +\n>>> +#ifdef CLAR_FIXTURE_PATH\n>>> +static const char *\n>>> +fixture_path(const char *base, const char *fixture_name);\n>>> +#endif\n>>> +\n>>> +struct clar_error {\n>>> +\tconst char *file;\n>>> +\tconst char *function;\n>>> +\tsize_t line_number;\n>>> +\tconst char *error_msg;\n>>> +\tchar *description;\n>>> +\n>>> +\tstruct clar_error *next;\n>>> +};\n>>> +\n>>> +struct clar_explicit {\n>>> +\tsize_t suite_idx;\n>>> +\tconst char *filter;\n>>> +\n>>> +\tstruct clar_explicit *next;\n>>> +};\n>>> +\n>>> +struct clar_report {\n>>> +\tconst char *test;\n>>> +\tint test_number;\n>>> +\tconst char *suite;\n>>> +\n>>> +\tenum cl_test_status status;\n>>> +\ttime_t start;\n>>> +\tdouble elapsed;\n>>> +\n>>> +\tstruct clar_error *errors;\n>>> +\tstruct clar_error *last_error;\n>>> +\n>>> +\tstruct clar_report *next;\n>>> +};\n>>> +\n>>> +struct clar_summary {\n>>> +\tconst char *filename;\n>>> +\tFILE *fp;\n>>> +};\n>>> +\n>>> +static struct {\n>>> +\tenum cl_test_status test_status;\n>>> +\n>>> +\tconst char *active_test;\n>>> +\tconst char *active_suite;\n>>> +\n>>> +\tint total_skipped;\n>>> +\tint total_errors;\n>>> +\n>>> +\tint tests_ran;\n>>> +\tint suites_ran;\n>>> +\n>>> +\tenum cl_output_format output_format;\n>>> +\n>>> +\tint report_errors_only;\n>>> +\tint exit_on_error;\n>>> +\tint verbosity;\n>>> +\n>>> +\tint write_summary;\n>>> +\tchar *summary_filename;\n>>> +\tstruct clar_summary *summary;\n>>> +\n>>> +\tstruct clar_explicit *explicit;\n>>> +\tstruct clar_explicit *last_explicit;\n>>> +\n>>> +\tstruct clar_report *reports;\n>>> +\tstruct clar_report *last_report;\n>>> +\n>>> +\tvoid (*local_cleanup)(void *);\n>>> +\tvoid *local_cleanup_payload;\n>>> +\n>>> +\tjmp_buf trampoline;\n>>> +\tint trampoline_enabled;\n>>> +\n>>> +\tcl_trace_cb *pfn_trace_cb;\n>>> +\tvoid *trace_payload;\n>>> +\n>>> +} _clar;\n>>> +\n>>> +struct clar_func {\n>>> +\tconst char *name;\n>>> +\tvoid (*ptr)(void);\n>>> +};\n>>> +\n>>> +struct clar_suite {\n>>> +\tconst char *name;\n>>> +\tstruct clar_func initialize;\n>>> +\tstruct clar_func cleanup;\n>>> +\tconst struct clar_func *tests;\n>>> +\tsize_t test_count;\n>>> +\tint enabled;\n>>> +};\n>>> +\n>>> +/* From clar_print_*.c */\n>>> +static void clar_print_init(int test_count, int suite_count, const char\n>>> *suite_names);\n>>> +static void clar_print_shutdown(int test_count, int suite_count, int\n>>> error_count);\n>>> +static void clar_print_error(int num, const struct clar_report *report,\n>>> const struct clar_error *error);\n>>> +static void clar_print_ontest(const char *suite_name, const char\n>>> *test_name, int test_number, enum cl_test_status failed);\n>>> +static void clar_print_onsuite(const char *suite_name, int suite_index);\n>>> +static void clar_print_onabort(const char *msg, ...);\n>>> +\n>>> +/* From clar_sandbox.c */\n>>> +static void clar_unsandbox(void);\n>>> +static int clar_sandbox(void);\n>>> +\n>>> +/* From summary.h */\n>>> +static struct clar_summary *clar_summary_init(const char *filename);\n>>> +static int clar_summary_shutdown(struct clar_summary *fp);\n>>> +\n>>> +/* Load the declarations for the test suite */\n>>> +#include \"clar.suite\"\n>>> +\n>>> +\n>>> +#define CL_TRACE(ev)\n>>> \\\n>>> +\tdo {\n>>> \\\n>>> +\t\tif (_clar.pfn_trace_cb)\n>>> \\\n>>> +\t\t\t_clar.pfn_trace_cb(ev,\n>>> \\\n>>> +\t\t\t\t\t\t\t   _clar.active_suite,\n>>> \\\n>>> +\t\t\t\t\t\t\t   _clar.active_test,\n>>> \\\n>>> +\n>>> _clar.trace_payload);\t\t\t\t\t\\\n>>> +\t} while (0)\n>>> +\n>>> +void cl_trace_register(cl_trace_cb *cb, void *payload)\n>>> +{\n>>> +\t_clar.pfn_trace_cb = cb;\n>>> +\t_clar.trace_payload = payload;\n>>> +}\n>>> +\n>>> +\n>>> +/* Core test functions */\n>>> +static void\n>>> +clar_report_errors(struct clar_report *report)\n>>> +{\n>>> +\tstruct clar_error *error;\n>>> +\tint i = 1;\n>>> +\n>>> +\tfor (error = report->errors; error; error = error->next)\n>>> +\t\tclar_print_error(i++, _clar.last_report, error);\n>>> +}\n>>> +\n>>> +static void\n>>> +clar_report_all(void)\n>>> +{\n>>> +\tstruct clar_report *report;\n>>> +\tstruct clar_error *error;\n>>> +\tint i = 1;\n>>> +\n>>> +\tfor (report = _clar.reports; report; report = report->next) {\n>>> +\t\tif (report->status != CL_TEST_FAILURE)\n>>> +\t\t\tcontinue;\n>>> +\n>>> +\t\tfor (error = report->errors; error; error = error->next)\n>>> +\t\t\tclar_print_error(i++, report, error);\n>>> +\t}\n>>> +}\n>>> +\n>>> +#ifdef WIN32\n>>> +# define clar_time DWORD\n>>> +\n>>> +static void clar_time_now(clar_time *out)\n>>> +{\n>>> +\t*out = GetTickCount();\n>>> +}\n>>> +\n>>> +static double clar_time_diff(clar_time *start, clar_time *end)\n>>> +{\n>>> +\treturn ((double)*end - (double)*start) / 1000;\n>>> +}\n>>> +#else\n>>> +# include <sys/time.h>\n>>> +\n>>> +# define clar_time struct timeval\n>>> +\n>>> +static void clar_time_now(clar_time *out)\n>>> +{\n>>> +\tstruct timezone tz;\n>>> +\n>>> +\tgettimeofday(out, &tz);\n>>> +}\n>>> +\n>>> +static double clar_time_diff(clar_time *start, clar_time *end)\n>>> +{\n>>> +\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n>>> +\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n>>> +}\n>>> +#endif\n>>> +\n>>> +static void\n>>> +clar_run_test(\n>>> +\tconst struct clar_suite *suite,\n>>> +\tconst struct clar_func *test,\n>>> +\tconst struct clar_func *initialize,\n>>> +\tconst struct clar_func *cleanup)\n>>> +{\n>>> +\tclar_time start, end;\n>>> +\n>>> +\t_clar.trampoline_enabled = 1;\n>>> +\n>>> +\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n>>> +\n>>> +\t_clar.last_report->start = time(NULL);\n>>> +\tclar_time_now(&start);\n>>> +\n>>> +\tif (setjmp(_clar.trampoline) == 0) {\n>>> +\t\tif (initialize->ptr != NULL)\n>>> +\t\t\tinitialize->ptr();\n>>> +\n>>> +\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n>>> +\t\ttest->ptr();\n>>> +\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n>>> +\t}\n>>> +\n>>> +\tclar_time_now(&end);\n>>> +\n>>> +\t_clar.trampoline_enabled = 0;\n>>> +\n>>> +\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n>>> +\t\t_clar.last_report->status = CL_TEST_OK;\n>>> +\n>>> +\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n>>> +\n>>> +\tif (_clar.local_cleanup != NULL)\n>>> +\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n>>> +\n>>> +\tif (cleanup->ptr != NULL)\n>>> +\t\tcleanup->ptr();\n>>> +\n>>> +\tCL_TRACE(CL_TRACE__TEST__END);\n>>> +\n>>> +\t_clar.tests_ran++;\n>>> +\n>>> +\t/* remove any local-set cleanup methods */\n>>> +\t_clar.local_cleanup = NULL;\n>>> +\t_clar.local_cleanup_payload = NULL;\n>>> +\n>>> +\tif (_clar.report_errors_only) {\n>>> +\t\tclar_report_errors(_clar.last_report);\n>>> +\t} else {\n>>> +\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran,\n>>> _clar.last_report->status);\n>>> +\t}\n>>> +}\n>>> +\n>>> +static void\n>>> +clar_run_suite(const struct clar_suite *suite, const char *filter)\n>>> +{\n>>> +\tconst struct clar_func *test = suite->tests;\n>>> +\tsize_t i, matchlen;\n>>> +\tstruct clar_report *report;\n>>> +\tint exact = 0;\n>>> +\n>>> +\tif (!suite->enabled)\n>>> +\t\treturn;\n>>> +\n>>> +\tif (_clar.exit_on_error && _clar.total_errors)\n>>> +\t\treturn;\n>>> +\n>>> +\tif (!_clar.report_errors_only)\n>>> +\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n>>> +\n>>> +\t_clar.active_suite = suite->name;\n>>> +\t_clar.active_test = NULL;\n>>> +\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n>>> +\n>>> +\tif (filter) {\n>>> +\t\tsize_t suitelen = strlen(suite->name);\n>>> +\t\tmatchlen = strlen(filter);\n>>> +\t\tif (matchlen <= suitelen) {\n>>> +\t\t\tfilter = NULL;\n>>> +\t\t} else {\n>>> +\t\t\tfilter += suitelen;\n>>> +\t\t\twhile (*filter == ':')\n>>> +\t\t\t\t++filter;\n>>> +\t\t\tmatchlen = strlen(filter);\n>>> +\n>>> +\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n>>> +\t\t\t\texact = 1;\n>>> +\t\t\t\tmatchlen--;\n>>> +\t\t\t}\n>>> +\t\t}\n>>> +\t}\n>>> +\n>>> +\tfor (i = 0; i < suite->test_count; ++i) {\n>>> +\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n>>> +\t\t\tcontinue;\n>>> +\n>>> +\t\tif (exact && strlen(test[i].name) != matchlen)\n>>> +\t\t\tcontinue;\n>>> +\n>>> +\t\t_clar.active_test = test[i].name;\n>>> +\n>>> +\t\treport = calloc(1, sizeof(struct clar_report));\n>>> +\t\treport->suite = _clar.active_suite;\n>>> +\t\treport->test = _clar.active_test;\n>>> +\t\treport->test_number = _clar.tests_ran;\n>>> +\t\treport->status = CL_TEST_NOTRUN;\n>>> +\n>>> +\t\tif (_clar.reports == NULL)\n>>> +\t\t\t_clar.reports = report;\n>>> +\n>>> +\t\tif (_clar.last_report != NULL)\n>>> +\t\t\t_clar.last_report->next = report;\n>>> +\n>>> +\t\t_clar.last_report = report;\n>>> +\n>>> +\t\tclar_run_test(suite, &test[i], &suite->initialize,\n>>> &suite->cleanup);\n>>> +\n>>> +\t\tif (_clar.exit_on_error && _clar.total_errors)\n>>> +\t\t\treturn;\n>>> +\t}\n>>> +\n>>> +\t_clar.active_test = NULL;\n>>> +\tCL_TRACE(CL_TRACE__SUITE_END);\n>>> +}\n>>> +\n>>> +static void\n>>> +clar_usage(const char *arg)\n>>> +{\n>>> +\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n>>> +\tprintf(\"Options:\\n\");\n>>> +\tprintf(\"  -sname        Run only the suite with `name` (can go to\n>>> individual test name)\\n\");\n>>> +\tprintf(\"  -iname        Include the suite with `name`\\n\");\n>>> +\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n>>> +\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n>>> +\tprintf(\"  -q            Only report tests that had an error\\n\");\n>>> +\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n>>> +\tprintf(\"  -t            Display results in tap format\\n\");\n>>> +\tprintf(\"  -l            Print suite names\\n\");\n>>> +\tprintf(\"  -r[filename]  Write summary file (to the optional\n>>> filename)\\n\");\n>>> +\texit(-1);\n>>> +}\n>>> +\n>>> +static void\n>>> +clar_parse_args(int argc, char **argv)\n>>> +{\n>>> +\tint i;\n>>> +\n>>> +\t/* Verify options before execute */\n>>> +\tfor (i = 1; i < argc; ++i) {\n>>> +\t\tchar *argument = argv[i];\n>>> +\n>>> +\t\tif (argument[0] != '-' || argument[1] == '\\0'\n>>> +\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n>>> +\t\t\tclar_usage(argv[0]);\n>>> +\t\t}\n>>> +\t}\n>>> +\n>>> +\tfor (i = 1; i < argc; ++i) {\n>>> +\t\tchar *argument = argv[i];\n>>> +\n>>> +\t\tswitch (argument[1]) {\n>>> +\t\tcase 's':\n>>> +\t\tcase 'i':\n>>> +\t\tcase 'x': { /* given suite name */\n>>> +\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n>>> +\t\t\tchar action = argument[1];\n>>> +\t\t\tsize_t j, arglen, suitelen, cmplen;\n>>> +\n>>> +\t\t\targument += offset;\n>>> +\t\t\targlen = strlen(argument);\n>>> +\n>>> +\t\t\tif (arglen == 0)\n>>> +\t\t\t\tclar_usage(argv[0]);\n>>> +\n>>> +\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n>>> +\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n>>> +\t\t\t\tcmplen = (arglen < suitelen) ? arglen :\n>>> suitelen;\n>>> +\n>>> +\t\t\t\tif (strncmp(argument, _clar_suites[j].name,\n>>> cmplen) == 0) {\n>>> +\t\t\t\t\tint exact = (arglen >= suitelen);\n>>> +\n>>> +\t\t\t\t\t/* Do we have a real suite prefix\n>>> separated by a\n>>> +\t\t\t\t\t * trailing '::' or just a matching\n>>> substring? */\n>>> +\t\t\t\t\tif (arglen > suitelen &&\n>>> (argument[suitelen] != ':'\n>>> +\t\t\t\t\t\t    || argument[suitelen + 1]\n>>> != ':'))\n>>> +\t\t\t\t\t    continue;\n>>> +\n>>> +\t\t\t\t\t++found;\n>>> +\n>>> +\t\t\t\t\tif (!exact)\n>>> +\t\t\t\t\t\t_clar.verbosity =\n>>> MAX(_clar.verbosity, 1);\n>>> +\n>>> +\t\t\t\t\tswitch (action) {\n>>> +\t\t\t\t\tcase 's': {\n>>> +\t\t\t\t\t\tstruct clar_explicit *explicit\n>>> =\n>>> +\t\t\t\t\t\t\tcalloc(1,\n>>> sizeof(struct clar_explicit));\n>>> +\t\t\t\t\t\tassert(explicit);\n>>> +\n>>> +\t\t\t\t\t\texplicit->suite_idx = j;\n>>> +\t\t\t\t\t\texplicit->filter = argument;\n>>> +\n>>> +\t\t\t\t\t\tif (_clar.explicit == NULL)\n>>> +\t\t\t\t\t\t\t_clar.explicit =\n>>> explicit;\n>>> +\n>>> +\t\t\t\t\t\tif (_clar.last_explicit !=\n>>> NULL)\n>>> +\n>>> _clar.last_explicit->next = explicit;\n>>> +\n>>> +\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n>>> +\t\t\t\t\t\t_clar.last_explicit =\n>>> explicit;\n>>> +\t\t\t\t\t\tbreak;\n>>> +\t\t\t\t\t}\n>>> +\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1;\n>>> break;\n>>> +\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0;\n>>> break;\n>>> +\t\t\t\t\t}\n>>> +\n>>> +\t\t\t\t\tif (exact)\n>>> +\t\t\t\t\t\tbreak;\n>>> +\t\t\t\t}\n>>> +\t\t\t}\n>>> +\n>>> +\t\t\tif (!found) {\n>>> +\t\t\t\tclar_print_onabort(\"No suite matching '%s'\n>>> found.\\n\", argument);\n>>> +\t\t\t\texit(-1);\n>>> +\t\t\t}\n>>> +\t\t\tbreak;\n>>> +\t\t}\n>>> +\n>>> +\t\tcase 'q':\n>>> +\t\t\t_clar.report_errors_only = 1;\n>>> +\t\t\tbreak;\n>>> +\n>>> +\t\tcase 'Q':\n>>> +\t\t\t_clar.exit_on_error = 1;\n>>> +\t\t\tbreak;\n>>> +\n>>> +\t\tcase 't':\n>>> +\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n>>> +\t\t\tbreak;\n>>> +\n>>> +\t\tcase 'l': {\n>>> +\t\t\tsize_t j;\n>>> +\t\t\tprintf(\"Test suites (use -s<name> to run just\n>>> one):\\n\");\n>>> +\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n>>> +\t\t\t\tprintf(\" %3d: %s\\n\", (int)j,\n>>> _clar_suites[j].name);\n>>> +\n>>> +\t\t\texit(0);\n>>> +\t\t}\n>>> +\n>>> +\t\tcase 'v':\n>>> +\t\t\t_clar.verbosity++;\n>>> +\t\t\tbreak;\n>>> +\n>>> +\t\tcase 'r':\n>>> +\t\t\t_clar.write_summary = 1;\n>>> +\t\t\tfree(_clar.summary_filename);\n>>> +\t\t\t_clar.summary_filename = *(argument + 2) ?\n>>> strdup(argument + 2) : NULL;\n>>> +\t\t\tbreak;\n>>> +\n>>> +\t\tdefault:\n>>> +\t\t\tassert(!\"Unexpected commandline argument!\");\n>>> +\t\t}\n>>> +\t}\n>>> +}\n>>> +\n>>> +void\n>>> +clar_test_init(int argc, char **argv)\n>>> +{\n>>> +\tconst char *summary_env;\n>>> +\n>>> +\tif (argc > 1)\n>>> +\t\tclar_parse_args(argc, argv);\n>>> +\n>>> +\tclar_print_init(\n>>> +\t\t(int)_clar_callback_count,\n>>> +\t\t(int)_clar_suite_count,\n>>> +\t\t\"\"\n>>> +\t);\n>>> +\n>>> +\tif (!_clar.summary_filename &&\n>>> +\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n>>> +\t\t_clar.write_summary = 1;\n>>> +\t\t_clar.summary_filename = strdup(summary_env);\n>>> +\t}\n>>> +\n>>> +\tif (_clar.write_summary && !_clar.summary_filename)\n>>> +\t\t_clar.summary_filename = strdup(\"summary.xml\");\n>>> +\n>>> +\tif (_clar.write_summary &&\n>>> +\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n>>> +\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n>>> +\t\texit(-1);\n>>> +\t}\n>>> +\n>>> +\tif (clar_sandbox() < 0) {\n>>> +\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n>>> +\t\texit(-1);\n>>> +\t}\n>>> +}\n>>> +\n>>> +int\n>>> +clar_test_run(void)\n>>> +{\n>>> +\tsize_t i;\n>>> +\tstruct clar_explicit *explicit;\n>>> +\n>>> +\tif (_clar.explicit) {\n>>> +\t\tfor (explicit = _clar.explicit; explicit; explicit =\n>>> explicit->next)\n>>> +\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx],\n>>> explicit->filter);\n>>> +\t} else {\n>>> +\t\tfor (i = 0; i < _clar_suite_count; ++i)\n>>> +\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n>>> +\t}\n>>> +\n>>> +\treturn _clar.total_errors;\n>>> +}\n>>> +\n>>> +void\n>>> +clar_test_shutdown(void)\n>>> +{\n>>> +\tstruct clar_explicit *explicit, *explicit_next;\n>>> +\tstruct clar_report *report, *report_next;\n>>> +\n>>> +\tclar_print_shutdown(\n>>> +\t\t_clar.tests_ran,\n>>> +\t\t(int)_clar_suite_count,\n>>> +\t\t_clar.total_errors\n>>> +\t);\n>>> +\n>>> +\tclar_unsandbox();\n>>> +\n>>> +\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n>>> +\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n>>> +\t\texit(-1);\n>>> +\t}\n>>> +\n>>> +\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n>>> +\t\texplicit_next = explicit->next;\n>>> +\t\tfree(explicit);\n>>> +\t}\n>>> +\n>>> +\tfor (report = _clar.reports; report; report = report_next) {\n>>> +\t\treport_next = report->next;\n>>> +\t\tfree(report);\n>>> +\t}\n>>> +\n>>> +\tfree(_clar.summary_filename);\n>>> +}\n>>> +\n>>> +int\n>>> +clar_test(int argc, char **argv)\n>>> +{\n>>> +\tint errors;\n>>> +\n>>> +\tclar_test_init(argc, argv);\n>>> +\terrors = clar_test_run();\n>>> +\tclar_test_shutdown();\n>>> +\n>>> +\treturn errors;\n>>> +}\n>>> +\n>>> +static void abort_test(void)\n>>> +{\n>>> +\tif (!_clar.trampoline_enabled) {\n>>> +\t\tclar_print_onabort(\n>>> +\t\t\t\t\"Fatal error: a cleanup method raised an\n>>> exception.\");\n>>> +\t\tclar_report_errors(_clar.last_report);\n>>> +\t\texit(-1);\n>>> +\t}\n>>> +\n>>> +\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n>>> +\tlongjmp(_clar.trampoline, -1);\n>>> +}\n>>> +\n>>> +void clar__skip(void)\n>>> +{\n>>> +\t_clar.last_report->status = CL_TEST_SKIP;\n>>> +\t_clar.total_skipped++;\n>>> +\tabort_test();\n>>> +}\n>>> +\n>>> +void clar__fail(\n>>> +\tconst char *file,\n>>> +\tconst char *function,\n>>> +\tsize_t line,\n>>> +\tconst char *error_msg,\n>>> +\tconst char *description,\n>>> +\tint should_abort)\n>>> +{\n>>> +\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n>>> +\n>>> +\tif (_clar.last_report->errors == NULL)\n>>> +\t\t_clar.last_report->errors = error;\n>>> +\n>>> +\tif (_clar.last_report->last_error != NULL)\n>>> +\t\t_clar.last_report->last_error->next = error;\n>>> +\n>>> +\t_clar.last_report->last_error = error;\n>>> +\n>>> +\terror->file = file;\n>>> +\terror->function = function;\n>>> +\terror->line_number = line;\n>>> +\terror->error_msg = error_msg;\n>>> +\n>>> +\tif (description != NULL)\n>>> +\t\terror->description = strdup(description);\n>>> +\n>>> +\t_clar.total_errors++;\n>>> +\t_clar.last_report->status = CL_TEST_FAILURE;\n>>> +\n>>> +\tif (should_abort)\n>>> +\t\tabort_test();\n>>> +}\n>>> +\n>>> +void clar__assert(\n>>> +\tint condition,\n>>> +\tconst char *file,\n>>> +\tconst char *function,\n>>> +\tsize_t line,\n>>> +\tconst char *error_msg,\n>>> +\tconst char *description,\n>>> +\tint should_abort)\n>>> +{\n>>> +\tif (condition)\n>>> +\t\treturn;\n>>> +\n>>> +\tclar__fail(file, function, line, error_msg, description,\n>>> should_abort);\n>>> +}\n>>> +\n>>> +void clar__assert_equal(\n>>> +\tconst char *file,\n>>> +\tconst char *function,\n>>> +\tsize_t line,\n>>> +\tconst char *err,\n>>> +\tint should_abort,\n>>> +\tconst char *fmt,\n>>> +\t...)\n>>> +{\n>>> +\tva_list args;\n>>> +\tchar buf[4096];\n>>> +\tint is_equal = 1;\n>>> +\n>>> +\tva_start(args, fmt);\n>>> +\n>>> +\tif (!strcmp(\"%s\", fmt)) {\n>>> +\t\tconst char *s1 = va_arg(args, const char *);\n>>> +\t\tconst char *s2 = va_arg(args, const char *);\n>>> +\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n>>> +\n>>> +\t\tif (!is_equal) {\n>>> +\t\t\tif (s1 && s2) {\n>>> +\t\t\t\tint pos;\n>>> +\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] &&\n>>> s2[pos]; ++pos)\n>>> +\t\t\t\t\t/* find differing byte offset */;\n>>> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at\n>>> byte %d)\",\n>>> +\t\t\t\t\ts1, s2, pos);\n>>> +\t\t\t} else {\n>>> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\",\n>>> s1, s2);\n>>> +\t\t\t}\n>>> +\t\t}\n>>> +\t}\n>>> +\telse if(!strcmp(\"%.*s\", fmt)) {\n>>> +\t\tconst char *s1 = va_arg(args, const char *);\n>>> +\t\tconst char *s2 = va_arg(args, const char *);\n>>> +\t\tint len = va_arg(args, int);\n>>> +\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n>>> +\n>>> +\t\tif (!is_equal) {\n>>> +\t\t\tif (s1 && s2) {\n>>> +\t\t\t\tint pos;\n>>> +\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len;\n>>> ++pos)\n>>> +\t\t\t\t\t/* find differing byte offset */;\n>>> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\n>>> (at byte %d)\",\n>>> +\t\t\t\t\tlen, s1, len, s2, pos);\n>>> +\t\t\t} else {\n>>> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' !=\n>>> '%.*s'\", len, s1, len, s2);\n>>> +\t\t\t}\n>>> +\t\t}\n>>> +\t}\n>>> +\telse if (!strcmp(\"%ls\", fmt)) {\n>>> +\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n>>> +\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n>>> +\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1,\n>>> wcs2);\n>>> +\n>>> +\t\tif (!is_equal) {\n>>> +\t\t\tif (wcs1 && wcs2) {\n>>> +\t\t\t\tint pos;\n>>> +\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] &&\n>>> wcs1[pos] && wcs2[pos]; ++pos)\n>>> +\t\t\t\t\t/* find differing byte offset */;\n>>> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\n>>> (at byte %d)\",\n>>> +\t\t\t\t\twcs1, wcs2, pos);\n>>> +\t\t\t} else {\n>>> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\",\n>>> wcs1, wcs2);\n>>> +\t\t\t}\n>>> +\t\t}\n>>> +\t}\n>>> +\telse if(!strcmp(\"%.*ls\", fmt)) {\n>>> +\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n>>> +\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n>>> +\t\tint len = va_arg(args, int);\n>>> +\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1,\n>>> wcs2, len);\n>>> +\n>>> +\t\tif (!is_equal) {\n>>> +\t\t\tif (wcs1 && wcs2) {\n>>> +\t\t\t\tint pos;\n>>> +\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos <\n>>> len; ++pos)\n>>> +\t\t\t\t\t/* find differing byte offset */;\n>>> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' !=\n>>> '%.*ls' (at byte %d)\",\n>>> +\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n>>> +\t\t\t} else {\n>>> +\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' !=\n>>> '%.*ls'\", len, wcs1, len, wcs2);\n>>> +\t\t\t}\n>>> +\t\t}\n>>> +\t}\n>>> +\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n>>> +\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n>>> +\t\tis_equal = (sz1 == sz2);\n>>> +\t\tif (!is_equal) {\n>>> +\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n>>> +\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n>>> +\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4,\n>>> fmt, sz2);\n>>> +\t\t}\n>>> +\t}\n>>> +\telse if (!strcmp(\"%p\", fmt)) {\n>>> +\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n>>> +\t\tis_equal = (p1 == p2);\n>>> +\t\tif (!is_equal)\n>>> +\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n>>> +\t}\n>>> +\telse {\n>>> +\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n>>> +\t\tis_equal = (i1 == i2);\n>>> +\t\tif (!is_equal) {\n>>> +\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n>>> +\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n>>> +\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4,\n>>> fmt, i2);\n>>> +\t\t}\n>>> +\t}\n>>> +\n>>> +\tva_end(args);\n>>> +\n>>> +\tif (!is_equal)\n>>> +\t\tclar__fail(file, function, line, err, buf, should_abort);\n>>> +}\n>>> +\n>>> +void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n>>> +{\n>>> +\t_clar.local_cleanup = cleanup;\n>>> +\t_clar.local_cleanup_payload = opaque;\n>>> +}\n>>> +\n>>> +#include \"clar/sandbox.h\"\n>>> +#include \"clar/fixtures.h\"\n>>> +#include \"clar/fs.h\"\n>>> +#include \"clar/print.h\"\n>>> +#include \"clar/summary.h\"\n>>> diff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\n>>> new file mode 100644\n>>> index 0000000000..8c22382bd5\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/clar.h\n>>> @@ -0,0 +1,173 @@\n>>> +/*\n>>> + * Copyright (c) Vicent Marti. All rights reserved.\n>>> + *\n>>> + * This file is part of clar, distributed under the ISC license.\n>>> + * For full terms see the included COPYING file.\n>>> + */\n>>> +#ifndef __CLAR_TEST_H__\n>>> +#define __CLAR_TEST_H__\n>>> +\n>>> +#include <stdlib.h>\n>>> +\n>>> +enum cl_test_status {\n>>> +\tCL_TEST_OK,\n>>> +\tCL_TEST_FAILURE,\n>>> +\tCL_TEST_SKIP,\n>>> +\tCL_TEST_NOTRUN,\n>>> +};\n>>> +\n>>> +enum cl_output_format {\n>>> +\tCL_OUTPUT_CLAP,\n>>> +\tCL_OUTPUT_TAP,\n>>> +};\n>>> +\n>>> +/** Setup clar environment */\n>>> +void clar_test_init(int argc, char *argv[]);\n>>> +int clar_test_run(void);\n>>> +void clar_test_shutdown(void);\n>>> +\n>>> +/** One shot setup & run */\n>>> +int clar_test(int argc, char *argv[]);\n>>> +\n>>> +const char *clar_sandbox_path(void);\n>>> +\n>>> +void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n>>> +void cl_fs_cleanup(void);\n>>> +\n>>> +/**\n>>> + * cl_trace_* is a hook to provide a simple global tracing\n>>> + * mechanism.\n>>> + *\n>>> + * The goal here is to let main() provide clar-proper\n>>> + * with a callback to optionally write log info for\n>>> + * test operations into the same stream used by their\n>>> + * actual tests.  This would let them print test names\n>>> + * and maybe performance data as they choose.\n>>> + *\n>>> + * The goal is NOT to alter the flow of control or to\n>>> + * override test selection/skipping.  (So the callback\n>>> + * does not return a value.)\n>>> + *\n>>> + * The goal is NOT to duplicate the existing\n>>> + * pass/fail/skip reporting.  (So the callback\n>>> + * does not accept a status/errorcode argument.)\n>>> + *\n>>> + */\n>>> +typedef enum cl_trace_event {\n>>> +\tCL_TRACE__SUITE_BEGIN,\n>>> +\tCL_TRACE__SUITE_END,\n>>> +\tCL_TRACE__TEST__BEGIN,\n>>> +\tCL_TRACE__TEST__END,\n>>> +\tCL_TRACE__TEST__RUN_BEGIN,\n>>> +\tCL_TRACE__TEST__RUN_END,\n>>> +\tCL_TRACE__TEST__LONGJMP,\n>>> +} cl_trace_event;\n>>> +\n>>> +typedef void (cl_trace_cb)(\n>>> +\tcl_trace_event ev,\n>>> +\tconst char *suite_name,\n>>> +\tconst char *test_name,\n>>> +\tvoid *payload);\n>>> +\n>>> +/**\n>>> + * Register a callback into CLAR to send global trace events.\n>>> + * Pass NULL to disable.\n>>> + */\n>>> +void cl_trace_register(cl_trace_cb *cb, void *payload);\n>>> +\n>>> +\n>>> +#ifdef CLAR_FIXTURE_PATH\n>>> +const char *cl_fixture(const char *fixture_name);\n>>> +void cl_fixture_sandbox(const char *fixture_name);\n>>> +void cl_fixture_cleanup(const char *fixture_name);\n>>> +const char *cl_fixture_basename(const char *fixture_name);\n>>> +#endif\n>>> +\n>>> +/**\n>>> + * Assertion macros with explicit error message\n>>> + */\n>>> +#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__,\n>>> __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n>>> +#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__,\n>>> __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n>>> +#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__,\n>>> __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n>>> +\n>>> +/**\n>>> + * Check macros with explicit error message\n>>> + */\n>>> +#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__,\n>>> __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n>>> +#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__,\n>>> __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n>>> +#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__,\n>>> __LINE__, \"Expression is not true: \" #expr, desc, 0)\n>>> +\n>>> +/**\n>>> + * Assertion macros with no error message\n>>> + */\n>>> +#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n>>> +#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n>>> +#define cl_assert(expr) cl_assert_(expr, NULL)\n>>> +\n>>> +/**\n>>> + * Check macros with no error message\n>>> + */\n>>> +#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n>>> +#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n>>> +#define cl_check(expr) cl_check_(expr, NULL)\n>>> +\n>>> +/**\n>>> + * Forced failure/warning\n>>> + */\n>>> +#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test\n>>> failed.\", desc, 1)\n>>> +#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning\n>>> during test execution:\", desc, 0)\n>>> +\n>>> +#define cl_skip() clar__skip()\n>>> +\n>>> +/**\n>>> + * Typed assertion macros\n>>> + */\n>>> +#define cl_assert_equal_s(s1,s2)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \"\n>>> #s2, 1, \"%s\", (s1), (s2))\n>>> +#define cl_assert_equal_s_(s1,s2,note)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \"\n>>> #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n>>> +\n>>> +#define cl_assert_equal_wcs(wcs1,wcs2)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" !=\n>>> \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n>>> +#define cl_assert_equal_wcs_(wcs1,wcs2,note)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" !=\n>>> \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n>>> +\n>>> +#define cl_assert_equal_strn(s1,s2,len)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \"\n>>> #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n>>> +#define cl_assert_equal_strn_(s1,s2,len,note)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \"\n>>> #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n>>> +\n>>> +#define cl_assert_equal_wcsn(wcs1,wcs2,len)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" !=\n>>> \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n>>> +#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" !=\n>>> \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n>>> +\n>>> +#define cl_assert_equal_i(i1,i2)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\",\n>>> (int)(i1), (int)(i2))\n>>> +#define cl_assert_equal_i_(i1,i2,note)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\",\n>>> 1, \"%d\", (i1), (i2))\n>>> +#define cl_assert_equal_i_fmt(i1,i2,fmt)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt),\n>>> (int)(i1), (int)(i2))\n>>> +\n>>> +#define cl_assert_equal_b(b1,b2)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\",\n>>> (int)((b1) != 0),(int)((b2) != 0))\n>>> +\n>>> +#define cl_assert_equal_p(p1,p2)\n>>> clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" !=\n>>> \" #p2, 1, \"%p\", (p1), (p2))\n>>> +\n>>> +void clar__skip(void);\n>>> +\n>>> +void clar__fail(\n>>> +\tconst char *file,\n>>> +\tconst char *func,\n>>> +\tsize_t line,\n>>> +\tconst char *error,\n>>> +\tconst char *description,\n>>> +\tint should_abort);\n>>> +\n>>> +void clar__assert(\n>>> +\tint condition,\n>>> +\tconst char *file,\n>>> +\tconst char *func,\n>>> +\tsize_t line,\n>>> +\tconst char *error,\n>>> +\tconst char *description,\n>>> +\tint should_abort);\n>>> +\n>>> +void clar__assert_equal(\n>>> +\tconst char *file,\n>>> +\tconst char *func,\n>>> +\tsize_t line,\n>>> +\tconst char *err,\n>>> +\tint should_abort,\n>>> +\tconst char *fmt,\n>>> +\t...);\n>>> +\n>>> +#endif\n>>> diff --git a/t/unit-tests/clar/clar/fixtures.h\n>>> b/t/unit-tests/clar/clar/fixtures.h\n>>> new file mode 100644\n>>> index 0000000000..6ec6423484\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/clar/fixtures.h\n>>> @@ -0,0 +1,50 @@\n>>> +#ifdef CLAR_FIXTURE_PATH\n>>> +static const char *\n>>> +fixture_path(const char *base, const char *fixture_name)\n>>> +{\n>>> +\tstatic char _path[4096];\n>>> +\tsize_t root_len;\n>>> +\n>>> +\troot_len = strlen(base);\n>>> +\tstrncpy(_path, base, sizeof(_path));\n>>> +\n>>> +\tif (_path[root_len - 1] != '/')\n>>> +\t\t_path[root_len++] = '/';\n>>> +\n>>> +\tif (fixture_name[0] == '/')\n>>> +\t\tfixture_name++;\n>>> +\n>>> +\tstrncpy(_path + root_len,\n>>> +\t\tfixture_name,\n>>> +\t\tsizeof(_path) - root_len);\n>>> +\n>>> +\treturn _path;\n>>> +}\n>>> +\n>>> +const char *cl_fixture(const char *fixture_name)\n>>> +{\n>>> +\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n>>> +}\n>>> +\n>>> +void cl_fixture_sandbox(const char *fixture_name)\n>>> +{\n>>> +\tfs_copy(cl_fixture(fixture_name), _clar_path);\n>>> +}\n>>> +\n>>> +const char *cl_fixture_basename(const char *fixture_name)\n>>> +{\n>>> +\tconst char *p;\n>>> +\n>>> +\tfor (p = fixture_name; *p; p++) {\n>>> +\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n>>> +\t\t\tfixture_name = p+1;\n>>> +\t}\n>>> +\n>>> +\treturn fixture_name;\n>>> +}\n>>> +\n>>> +void cl_fixture_cleanup(const char *fixture_name)\n>>> +{\n>>> +\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n>>> +}\n>>> +#endif\n>>> diff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\n>>> new file mode 100644\n>>> index 0000000000..3e39890bd3\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/clar/fs.h\n>>> @@ -0,0 +1,522 @@\n>>> +/*\n>>> + * By default, use a read/write loop to copy files on POSIX systems.\n>>> + * On Linux, use sendfile by default as it's slightly faster.  On\n>>> + * macOS, we avoid fcopyfile by default because it's slightly slower.\n>>> + */\n>>> +#undef USE_FCOPYFILE\n>>> +#define USE_SENDFILE 1\n>>> +\n>>> +#ifdef _WIN32\n>>> +\n>>> +#ifdef CLAR_WIN32_LONGPATHS\n>>> +# define CLAR_MAX_PATH 4096\n>>> +#else\n>>> +# define CLAR_MAX_PATH MAX_PATH\n>>> +#endif\n>>> +\n>>> +#define RM_RETRY_COUNT\t5\n>>> +#define RM_RETRY_DELAY\t10\n>>> +\n>>> +#ifdef __MINGW32__\n>>> +\n>>> +/* These security-enhanced functions are not available\n>>> + * in MinGW, so just use the vanilla ones */\n>>> +#define wcscpy_s(a, b, c) wcscpy((a), (c))\n>>> +#define wcscat_s(a, b, c) wcscat((a), (c))\n>>> +\n>>> +#endif /* __MINGW32__ */\n>>> +\n>>> +static int\n>>> +fs__dotordotdot(WCHAR *_tocheck)\n>>> +{\n>>> +\treturn _tocheck[0] == '.' &&\n>>> +\t\t(_tocheck[1] == '\\0' ||\n>>> +\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n>>> +}\n>>> +\n>>> +static int\n>>> +fs_rmdir_rmdir(WCHAR *_wpath)\n>>> +{\n>>> +\tunsigned retries = 1;\n>>> +\n>>> +\twhile (!RemoveDirectoryW(_wpath)) {\n>>> +\t\t/* Only retry when we have retries remaining, and the\n>>> +\t\t * error was ERROR_DIR_NOT_EMPTY. */\n>>> +\t\tif (retries++ > RM_RETRY_COUNT ||\n>>> +\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n>>> +\t\t\treturn -1;\n>>> +\n>>> +\t\t/* Give whatever has a handle to a child item some time\n>>> +\t\t * to release it before trying again */\n>>> +\t\tSleep(RM_RETRY_DELAY * retries * retries);\n>>> +\t}\n>>> +\n>>> +\treturn 0;\n>>> +}\n>>> +\n>>> +static void translate_path(WCHAR *path, size_t path_size)\n>>> +{\n>>> +    size_t path_len, i;\n>>> +\n>>> +    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n>>> +\treturn;\n>>> +\n>>> +    path_len = wcslen(path);\n>>> +    cl_assert(path_size > path_len + 4);\n>>> +\n>>> +    for (i = path_len; i > 0; i--) {\n>>> +\tWCHAR c = path[i - 1];\n>>> +\n>>> +\tif (c == L'/')\n>>> +\t    path[i + 3] = L'\\\\';\n>>> +\telse\n>>> +\t    path[i + 3] = path[i - 1];\n>>> +    }\n>>> +\n>>> +    path[0] = L'\\\\';\n>>> +    path[1] = L'\\\\';\n>>> +    path[2] = L'?';\n>>> +    path[3] = L'\\\\';\n>>> +    path[path_len + 4] = L'\\0';\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_rmdir_helper(WCHAR *_wsource)\n>>> +{\n>>> +\tWCHAR buffer[CLAR_MAX_PATH];\n>>> +\tHANDLE find_handle;\n>>> +\tWIN32_FIND_DATAW find_data;\n>>> +\tsize_t buffer_prefix_len;\n>>> +\n>>> +\t/* Set up the buffer and capture the length */\n>>> +\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n>>> +\ttranslate_path(buffer, CLAR_MAX_PATH);\n>>> +\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n>>> +\tbuffer_prefix_len = wcslen(buffer);\n>>> +\n>>> +\t/* FindFirstFile needs a wildcard to match multiple items */\n>>> +\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n>>> +\tfind_handle = FindFirstFileW(buffer, &find_data);\n>>> +\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n>>> +\n>>> +\tdo {\n>>> +\t\t/* FindFirstFile/FindNextFile gives back . and ..\n>>> +\t\t * entries at the beginning */\n>>> +\t\tif (fs__dotordotdot(find_data.cFileName))\n>>> +\t\t\tcontinue;\n>>> +\n>>> +\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH -\n>>> buffer_prefix_len, find_data.cFileName);\n>>> +\n>>> +\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n>>> +\t\t\tfs_rmdir_helper(buffer);\n>>> +\t\telse {\n>>> +\t\t\t/* If set, the +R bit must be cleared before deleting\n>>> */\n>>> +\t\t\tif (FILE_ATTRIBUTE_READONLY &\n>>> find_data.dwFileAttributes)\n>>> +\t\t\t\tcl_assert(SetFileAttributesW(buffer,\n>>> find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n>>> +\n>>> +\t\t\tcl_assert(DeleteFileW(buffer));\n>>> +\t\t}\n>>> +\t}\n>>> +\twhile (FindNextFileW(find_handle, &find_data));\n>>> +\n>>> +\t/* Ensure that we successfully completed the enumeration */\n>>> +\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n>>> +\n>>> +\t/* Close the find handle */\n>>> +\tFindClose(find_handle);\n>>> +\n>>> +\t/* Now that the directory is empty, remove it */\n>>> +\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n>>> +}\n>>> +\n>>> +static int\n>>> +fs_rm_wait(WCHAR *_wpath)\n>>> +{\n>>> +\tunsigned retries = 1;\n>>> +\tDWORD last_error;\n>>> +\n>>> +\tdo {\n>>> +\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n>>> +\t\t\tlast_error = GetLastError();\n>>> +\t\telse\n>>> +\t\t\tlast_error = ERROR_SUCCESS;\n>>> +\n>>> +\t\t/* Is the item gone? */\n>>> +\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n>>> +\t\t\tERROR_PATH_NOT_FOUND == last_error)\n>>> +\t\t\treturn 0;\n>>> +\n>>> +\t\tSleep(RM_RETRY_DELAY * retries * retries);\n>>> +\t}\n>>> +\twhile (retries++ <= RM_RETRY_COUNT);\n>>> +\n>>> +\treturn -1;\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_rm(const char *_source)\n>>> +{\n>>> +\tWCHAR wsource[CLAR_MAX_PATH];\n>>> +\tDWORD attrs;\n>>> +\n>>> +\t/* The input path is UTF-8. Convert it to wide characters\n>>> +\t * for use with the Windows API */\n>>> +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n>>> +\t\t\t\tMB_ERR_INVALID_CHARS,\n>>> +\t\t\t\t_source,\n>>> +\t\t\t\t-1, /* Indicates NULL termination */\n>>> +\t\t\t\twsource,\n>>> +\t\t\t\tCLAR_MAX_PATH));\n>>> +\n>>> +\ttranslate_path(wsource, CLAR_MAX_PATH);\n>>> +\n>>> +\t/* Does the item exist? If not, we have no work to do */\n>>> +\tattrs = GetFileAttributesW(wsource);\n>>> +\n>>> +\tif (INVALID_FILE_ATTRIBUTES == attrs)\n>>> +\t\treturn;\n>>> +\n>>> +\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n>>> +\t\tfs_rmdir_helper(wsource);\n>>> +\telse {\n>>> +\t\t/* The item is a file. Strip the +R bit */\n>>> +\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n>>> +\t\t\tcl_assert(SetFileAttributesW(wsource, attrs &\n>>> ~FILE_ATTRIBUTE_READONLY));\n>>> +\n>>> +\t\tcl_assert(DeleteFileW(wsource));\n>>> +\t}\n>>> +\n>>> +\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n>>> +\tcl_assert(0 == fs_rm_wait(wsource));\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n>>> +{\n>>> +\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n>>> +\tHANDLE find_handle;\n>>> +\tWIN32_FIND_DATAW find_data;\n>>> +\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n>>> +\n>>> +\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n>>> +\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n>>> +\ttranslate_path(buf_source, CLAR_MAX_PATH);\n>>> +\tbuf_source_prefix_len = wcslen(buf_source);\n>>> +\n>>> +\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n>>> +\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n>>> +\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n>>> +\tbuf_dest_prefix_len = wcslen(buf_dest);\n>>> +\n>>> +\t/* Get an enumerator for the items in the source. */\n>>> +\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n>>> +\tfind_handle = FindFirstFileW(buf_source, &find_data);\n>>> +\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n>>> +\n>>> +\t/* Create the target directory. */\n>>> +\tcl_assert(CreateDirectoryW(_wdest, NULL));\n>>> +\n>>> +\tdo {\n>>> +\t\t/* FindFirstFile/FindNextFile gives back . and ..\n>>> +\t\t * entries at the beginning */\n>>> +\t\tif (fs__dotordotdot(find_data.cFileName))\n>>> +\t\t\tcontinue;\n>>> +\n>>> +\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH -\n>>> buf_source_prefix_len, find_data.cFileName);\n>>> +\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH -\n>>> buf_dest_prefix_len, find_data.cFileName);\n>>> +\n>>> +\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n>>> +\t\t\tfs_copydir_helper(buf_source, buf_dest);\n>>> +\t\telse\n>>> +\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n>>> +\t}\n>>> +\twhile (FindNextFileW(find_handle, &find_data));\n>>> +\n>>> +\t/* Ensure that we successfully completed the enumeration */\n>>> +\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n>>> +\n>>> +\t/* Close the find handle */\n>>> +\tFindClose(find_handle);\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_copy(const char *_source, const char *_dest)\n>>> +{\n>>> +\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n>>> +\tDWORD source_attrs, dest_attrs;\n>>> +\tHANDLE find_handle;\n>>> +\tWIN32_FIND_DATAW find_data;\n>>> +\n>>> +\t/* The input paths are UTF-8. Convert them to wide characters\n>>> +\t * for use with the Windows API. */\n>>> +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n>>> +\t\t\t\tMB_ERR_INVALID_CHARS,\n>>> +\t\t\t\t_source,\n>>> +\t\t\t\t-1,\n>>> +\t\t\t\twsource,\n>>> +\t\t\t\tCLAR_MAX_PATH));\n>>> +\n>>> +\tcl_assert(MultiByteToWideChar(CP_UTF8,\n>>> +\t\t\t\tMB_ERR_INVALID_CHARS,\n>>> +\t\t\t\t_dest,\n>>> +\t\t\t\t-1,\n>>> +\t\t\t\twdest,\n>>> +\t\t\t\tCLAR_MAX_PATH));\n>>> +\n>>> +\ttranslate_path(wsource, CLAR_MAX_PATH);\n>>> +\ttranslate_path(wdest, CLAR_MAX_PATH);\n>>> +\n>>> +\t/* Check the source for existence */\n>>> +\tsource_attrs = GetFileAttributesW(wsource);\n>>> +\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n>>> +\n>>> +\t/* Check the target for existence */\n>>> +\tdest_attrs = GetFileAttributesW(wdest);\n>>> +\n>>> +\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n>>> +\t\t/* Target exists; append last path part of source to target.\n>>> +\t\t * Use FindFirstFile to parse the path */\n>>> +\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n>>> +\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n>>> +\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n>>> +\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n>>> +\t\tFindClose(find_handle);\n>>> +\n>>> +\t\t/* Check the new target for existence */\n>>> +\t\tcl_assert(INVALID_FILE_ATTRIBUTES ==\n>>> GetFileAttributesW(wdest));\n>>> +\t}\n>>> +\n>>> +\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n>>> +\t\tfs_copydir_helper(wsource, wdest);\n>>> +\telse\n>>> +\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n>>> +}\n>>> +\n>>> +void\n>>> +cl_fs_cleanup(void)\n>>> +{\n>>> +#ifdef CLAR_FIXTURE_PATH\n>>> +\tfs_rm(fixture_path(_clar_path, \"*\"));\n>>> +#endif\n>>> +}\n>>> +\n>>> +#else\n>>> +\n>>> +#include <errno.h>\n>>> +#include <string.h>\n>>> +#include <limits.h>\n>>> +#include <dirent.h>\n>>> +#include <fcntl.h>\n>>> +#include <unistd.h>\n>>> +#include <sys/types.h>\n>>> +#include <sys/stat.h>\n>>> +\n>>> +#if defined(__linux__)\n>>> +# include <sys/sendfile.h>\n>>> +#endif\n>>> +\n>>> +#if defined(__APPLE__)\n>>> +# include <copyfile.h>\n>>> +#endif\n>>> +\n>>> +static void basename_r(const char **out, int *out_len, const char *in)\n>>> +{\n>>> +\tsize_t in_len = strlen(in), start_pos;\n>>> +\n>>> +\tfor (in_len = strlen(in); in_len; in_len--) {\n>>> +\t\tif (in[in_len - 1] != '/')\n>>> +\t\t\tbreak;\n>>> +\t}\n>>> +\n>>> +\tfor (start_pos = in_len; start_pos; start_pos--) {\n>>> +\t\tif (in[start_pos - 1] == '/')\n>>> +\t\t\tbreak;\n>>> +\t}\n>>> +\n>>> +\tcl_assert(in_len - start_pos < INT_MAX);\n>>> +\n>>> +\tif (in_len - start_pos > 0) {\n>>> +\t\t*out = &in[start_pos];\n>>> +\t\t*out_len = (in_len - start_pos);\n>>> +\t} else {\n>>> +\t\t*out = \"/\";\n>>> +\t\t*out_len = 1;\n>>> +\t}\n>>> +}\n>>> +\n>>> +static char *joinpath(const char *dir, const char *base, int base_len)\n>>> +{\n>>> +\tchar *out;\n>>> +\tint len;\n>>> +\n>>> +\tif (base_len == -1) {\n>>> +\t\tsize_t bl = strlen(base);\n>>> +\n>>> +\t\tcl_assert(bl < INT_MAX);\n>>> +\t\tbase_len = (int)bl;\n>>> +\t}\n>>> +\n>>> +\tlen = strlen(dir) + base_len + 2;\n>>> +\tcl_assert(len > 0);\n>>> +\n>>> +\tcl_assert(out = malloc(len));\n>>> +\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n>>> +\n>>> +\treturn out;\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n>>> +{\n>>> +\tDIR *source_dir;\n>>> +\tstruct dirent *d;\n>>> +\n>>> +\tmkdir(dest, dest_mode);\n>>> +\n>>> +\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n>>> +\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n>>> +\t\tchar *child;\n>>> +\n>>> +\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n>>> +\t\t\tcontinue;\n>>> +\n>>> +\t\tchild = joinpath(source, d->d_name, -1);\n>>> +\t\tfs_copy(child, dest);\n>>> +\t\tfree(child);\n>>> +\t}\n>>> +\n>>> +\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n>>> +\n>>> +\tclosedir(source_dir);\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_copyfile_helper(const char *source, size_t source_len, const char *dest,\n>>> int dest_mode)\n>>> +{\n>>> +\tint in, out;\n>>> +\n>>> +\tcl_must_pass((in = open(source, O_RDONLY)));\n>>> +\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n>>> +\n>>> +#if USE_FCOPYFILE && defined(__APPLE__)\n>>> +\t((void)(source_len)); /* unused */\n>>> +\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n>>> +#elif USE_SENDFILE && defined(__linux__)\n>>> +\t{\n>>> +\t\tssize_t ret = 0;\n>>> +\n>>> +\t\twhile (source_len && (ret = sendfile(out, in, NULL,\n>>> source_len)) > 0) {\n>>> +\t\t\tsource_len -= (size_t)ret;\n>>> +\t\t}\n>>> +\t\tcl_assert(ret >= 0);\n>>> +\t}\n>>> +#else\n>>> +\t{\n>>> +\t\tchar buf[131072];\n>>> +\t\tssize_t ret;\n>>> +\n>>> +\t\t((void)(source_len)); /* unused */\n>>> +\n>>> +\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n>>> +\t\t\tsize_t len = (size_t)ret;\n>>> +\n>>> +\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n>>> +\t\t\t\tcl_assert(ret <= (ssize_t)len);\n>>> +\t\t\t\tlen -= ret;\n>>> +\t\t\t}\n>>> +\t\t\tcl_assert(ret >= 0);\n>>> +\t\t}\n>>> +\t\tcl_assert(ret == 0);\n>>> +\t}\n>>> +#endif\n>>> +\n>>> +\tclose(in);\n>>> +\tclose(out);\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_copy(const char *source, const char *_dest)\n>>> +{\n>>> +\tchar *dbuf = NULL;\n>>> +\tconst char *dest = NULL;\n>>> +\tstruct stat source_st, dest_st;\n>>> +\n>>> +\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy\n>>> source\");\n>>> +\n>>> +\tif (lstat(_dest, &dest_st) == 0) {\n>>> +\t\tconst char *base;\n>>> +\t\tint base_len;\n>>> +\n>>> +\t\t/* Target exists and is directory; append basename */\n>>> +\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n>>> +\n>>> +\t\tbasename_r(&base, &base_len, source);\n>>> +\t\tcl_assert(base_len < INT_MAX);\n>>> +\n>>> +\t\tdbuf = joinpath(_dest, base, base_len);\n>>> +\t\tdest = dbuf;\n>>> +\t} else if (errno != ENOENT) {\n>>> +\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n>>> +\t} else {\n>>> +\t\tdest = _dest;\n>>> +\t}\n>>> +\n>>> +\tif (S_ISDIR(source_st.st_mode)) {\n>>> +\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n>>> +\t} else {\n>>> +\t\tfs_copyfile_helper(source, source_st.st_size, dest,\n>>> source_st.st_mode);\n>>> +\t}\n>>> +\n>>> +\tfree(dbuf);\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_rmdir_helper(const char *path)\n>>> +{\n>>> +\tDIR *dir;\n>>> +\tstruct dirent *d;\n>>> +\n>>> +\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n>>> +\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n>>> +\t\tchar *child;\n>>> +\n>>> +\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n>>> +\t\t\tcontinue;\n>>> +\n>>> +\t\tchild = joinpath(path, d->d_name, -1);\n>>> +\t\tfs_rm(child);\n>>> +\t\tfree(child);\n>>> +\t}\n>>> +\n>>> +\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n>>> +\tclosedir(dir);\n>>> +\n>>> +\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n>>> +}\n>>> +\n>>> +static void\n>>> +fs_rm(const char *path)\n>>> +{\n>>> +\tstruct stat st;\n>>> +\n>>> +\tif (lstat(path, &st)) {\n>>> +\t\tif (errno == ENOENT)\n>>> +\t\t\treturn;\n>>> +\n>>> +\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n>>> +\t}\n>>> +\n>>> +\tif (S_ISDIR(st.st_mode)) {\n>>> +\t\tfs_rmdir_helper(path);\n>>> +\t} else {\n>>> +\t\tcl_must_pass(unlink(path));\n>>> +\t}\n>>> +}\n>>> +\n>>> +void\n>>> +cl_fs_cleanup(void)\n>>> +{\n>>> +\tclar_unsandbox();\n>>> +\tclar_sandbox();\n>>> +}\n>>> +#endif\n>>> diff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\n>>> new file mode 100644\n>>> index 0000000000..c17e2f693b\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/clar/print.h\n>>> @@ -0,0 +1,211 @@\n>>> +/* clap: clar protocol, the traditional clar output format */\n>>> +\n>>> +static void clar_print_clap_init(int test_count, int suite_count, const\n>>> char *suite_names)\n>>> +{\n>>> +\t(void)test_count;\n>>> +\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n>>> +\tprintf(\"Started (test status codes: OK='.' FAILURE='F'\n>>> SKIPPED='S')\\n\");\n>>> +}\n>>> +\n>>> +static void clar_print_clap_shutdown(int test_count, int suite_count, int\n>>> error_count)\n>>> +{\n>>> +\t(void)test_count;\n>>> +\t(void)suite_count;\n>>> +\t(void)error_count;\n>>> +\n>>> +\tprintf(\"\\n\\n\");\n>>> +\tclar_report_all();\n>>> +}\n>>> +\n>>> +static void clar_print_clap_error(int num, const struct clar_report\n>>> *report, const struct clar_error *error)\n>>> +{\n>>> +\tprintf(\"  %d) Failure:\\n\", num);\n>>> +\n>>> +\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n>>> +\t\treport->suite,\n>>> +\t\treport->test,\n>>> +\t\terror->file,\n>>> +\t\terror->line_number);\n>>> +\n>>> +\tprintf(\"  %s\\n\", error->error_msg);\n>>> +\n>>> +\tif (error->description != NULL)\n>>> +\t\tprintf(\"  %s\\n\", error->description);\n>>> +\n>>> +\tprintf(\"\\n\");\n>>> +\tfflush(stdout);\n>>> +}\n>>> +\n>>> +static void clar_print_clap_ontest(const char *suite_name, const char\n>>> *test_name, int test_number, enum cl_test_status status)\n>>> +{\n>>> +\t(void)test_name;\n>>> +\t(void)test_number;\n>>> +\n>>> +\tif (_clar.verbosity > 1) {\n>>> +\t\tprintf(\"%s::%s: \", suite_name, test_name);\n>>> +\n>>> +\t\tswitch (status) {\n>>> +\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n>>> +\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n>>> +\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n>>> +\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n>>> +\t\t}\n>>> +\t} else {\n>>> +\t\tswitch (status) {\n>>> +\t\tcase CL_TEST_OK: printf(\".\"); break;\n>>> +\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n>>> +\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n>>> +\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n>>> +\t\t}\n>>> +\n>>> +\t\tfflush(stdout);\n>>> +\t}\n>>> +}\n>>> +\n>>> +static void clar_print_clap_onsuite(const char *suite_name, int\n>>> suite_index)\n>>> +{\n>>> +\tif (_clar.verbosity == 1)\n>>> +\t\tprintf(\"\\n%s\", suite_name);\n>>> +\n>>> +\t(void)suite_index;\n>>> +}\n>>> +\n>>> +static void clar_print_clap_onabort(const char *fmt, va_list arg)\n>>> +{\n>>> +\tvfprintf(stderr, fmt, arg);\n>>> +}\n>>> +\n>>> +/* tap: test anywhere protocol format */\n>>> +\n>>> +static void clar_print_tap_init(int test_count, int suite_count, const char\n>>> *suite_names)\n>>> +{\n>>> +\t(void)test_count;\n>>> +\t(void)suite_count;\n>>> +\t(void)suite_names;\n>>> +\tprintf(\"TAP version 13\\n\");\n>>> +}\n>>> +\n>>> +static void clar_print_tap_shutdown(int test_count, int suite_count, int\n>>> error_count)\n>>> +{\n>>> +\t(void)suite_count;\n>>> +\t(void)error_count;\n>>> +\n>>> +\tprintf(\"1..%d\\n\", test_count);\n>>> +}\n>>> +\n>>> +static void clar_print_tap_error(int num, const struct clar_report *report,\n>>> const struct clar_error *error)\n>>> +{\n>>> +\t(void)num;\n>>> +\t(void)report;\n>>> +\t(void)error;\n>>> +}\n>>> +\n>>> +static void print_escaped(const char *str)\n>>> +{\n>>> +\tchar *c;\n>>> +\n>>> +\twhile ((c = strchr(str, '\\'')) != NULL) {\n>>> +\t\tprintf(\"%.*s\", (int)(c - str), str);\n>>> +\t\tprintf(\"''\");\n>>> +\t\tstr = c + 1;\n>>> +\t}\n>>> +\n>>> +\tprintf(\"%s\", str);\n>>> +}\n>>> +\n>>> +static void clar_print_tap_ontest(const char *suite_name, const char\n>>> *test_name, int test_number, enum cl_test_status status)\n>>> +{\n>>> +\tconst struct clar_error *error = _clar.last_report->errors;\n>>> +\n>>> +\t(void)test_name;\n>>> +\t(void)test_number;\n>>> +\n>>> +\tswitch(status) {\n>>> +\tcase CL_TEST_OK:\n>>> +\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name,\n>>> test_name);\n>>> +\t\tbreak;\n>>> +\tcase CL_TEST_FAILURE:\n>>> +\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name,\n>>> test_name);\n>>> +\n>>> +\t\tprintf(\"    ---\\n\");\n>>> +\t\tprintf(\"    reason: |\\n\");\n>>> +\t\tprintf(\"      %s\\n\", error->error_msg);\n>>> +\n>>> +\t\tif (error->description)\n>>> +\t\t\tprintf(\"      %s\\n\", error->description);\n>>> +\n>>> +\t\tprintf(\"    at:\\n\");\n>>> +\t\tprintf(\"      file: '\"); print_escaped(error->file);\n>>> printf(\"'\\n\");\n>>> +\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n>>> +\t\tprintf(\"      function: '%s'\\n\", error->function);\n>>> +\t\tprintf(\"    ---\\n\");\n>>> +\n>>> +\t\tbreak;\n>>> +\tcase CL_TEST_SKIP:\n>>> +\tcase CL_TEST_NOTRUN:\n>>> +\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name,\n>>> test_name);\n>>> +\t\tbreak;\n>>> +\t}\n>>> +\n>>> +\tfflush(stdout);\n>>> +}\n>>> +\n>>> +static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n>>> +{\n>>> +\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n>>> +}\n>>> +\n>>> +static void clar_print_tap_onabort(const char *fmt, va_list arg)\n>>> +{\n>>> +\tprintf(\"Bail out! \");\n>>> +\tvprintf(fmt, arg);\n>>> +\tfflush(stdout);\n>>> +}\n>>> +\n>>> +/* indirection between protocol output selection */\n>>> +\n>>> +#define PRINT(FN, ...) do { \\\n>>> +\t\tswitch (_clar.output_format) { \\\n>>> +\t\t\tcase CL_OUTPUT_CLAP: \\\n>>> +\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n>>> +\t\t\t\tbreak; \\\n>>> +\t\t\tcase CL_OUTPUT_TAP: \\\n>>> +\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n>>> +\t\t\t\tbreak; \\\n>>> +\t\t\tdefault: \\\n>>> +\t\t\t\tabort(); \\\n>>> +\t\t} \\\n>>> +\t} while (0)\n>>> +\n>>> +static void clar_print_init(int test_count, int suite_count, const char\n>>> *suite_names)\n>>> +{\n>>> +\tPRINT(init, test_count, suite_count, suite_names);\n>>> +}\n>>> +\n>>> +static void clar_print_shutdown(int test_count, int suite_count, int\n>>> error_count)\n>>> +{\n>>> +\tPRINT(shutdown, test_count, suite_count, error_count);\n>>> +}\n>>> +\n>>> +static void clar_print_error(int num, const struct clar_report *report,\n>>> const struct clar_error *error)\n>>> +{\n>>> +\tPRINT(error, num, report, error);\n>>> +}\n>>> +\n>>> +static void clar_print_ontest(const char *suite_name, const char\n>>> *test_name, int test_number, enum cl_test_status status)\n>>> +{\n>>> +\tPRINT(ontest, suite_name, test_name, test_number, status);\n>>> +}\n>>> +\n>>> +static void clar_print_onsuite(const char *suite_name, int suite_index)\n>>> +{\n>>> +\tPRINT(onsuite, suite_name, suite_index);\n>>> +}\n>>> +\n>>> +static void clar_print_onabort(const char *msg, ...)\n>>> +{\n>>> +\tva_list argp;\n>>> +\tva_start(argp, msg);\n>>> +\tPRINT(onabort, msg, argp);\n>>> +\tva_end(argp);\n>>> +}\n>>> diff --git a/t/unit-tests/clar/clar/sandbox.h\n>>> b/t/unit-tests/clar/clar/sandbox.h\n>>> new file mode 100644\n>>> index 0000000000..7c177f3525\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/clar/sandbox.h\n>>> @@ -0,0 +1,153 @@\n>>> +#ifdef __APPLE__\n>>> +#include <sys/syslimits.h>\n>>> +#endif\n>>> +\n>>> +static char _clar_path[4096 + 1];\n>>> +\n>>> +static int\n>>> +is_valid_tmp_path(const char *path)\n>>> +{\n>>> +\tSTAT_T st;\n>>> +\n>>> +\tif (stat(path, &st) != 0)\n>>> +\t\treturn 0;\n>>> +\n>>> +\tif (!S_ISDIR(st.st_mode))\n>>> +\t\treturn 0;\n>>> +\n>>> +\treturn (access(path, W_OK) == 0);\n>>> +}\n>>> +\n>>> +static int\n>>> +find_tmp_path(char *buffer, size_t length)\n>>> +{\n>>> +#ifndef _WIN32\n>>> +\tstatic const size_t var_count = 5;\n>>> +\tstatic const char *env_vars[] = {\n>>> +\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n>>> +\t};\n>>> +\n>>> +\tsize_t i;\n>>> +\n>>> +\tfor (i = 0; i < var_count; ++i) {\n>>> +\t\tconst char *env = getenv(env_vars[i]);\n>>> +\t\tif (!env)\n>>> +\t\t\tcontinue;\n>>> +\n>>> +\t\tif (is_valid_tmp_path(env)) {\n>>> +#ifdef __APPLE__\n>>> +\t\t\tif (length >= PATH_MAX && realpath(env, buffer) !=\n>>> NULL)\n>>> +\t\t\t\treturn 0;\n>>> +#endif\n>>> +\t\t\tstrncpy(buffer, env, length - 1);\n>>> +\t\t\tbuffer[length - 1] = '\\0';\n>>> +\t\t\treturn 0;\n>>> +\t\t}\n>>> +\t}\n>>> +\n>>> +\t/* If the environment doesn't say anything, try to use /tmp */\n>>> +\tif (is_valid_tmp_path(\"/tmp\")) {\n>>> +#ifdef __APPLE__\n>>> +\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n>>> +\t\t\treturn 0;\n>>> +#endif\n>>> +\t\tstrncpy(buffer, \"/tmp\", length - 1);\n>>> +\t\tbuffer[length - 1] = '\\0';\n>>> +\t\treturn 0;\n>>> +\t}\n>>> +\n>>> +#else\n>>> +\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer,\n>>> (DWORD)length);\n>>> +\tif (env_len > 0 && env_len < (DWORD)length)\n>>> +\t\treturn 0;\n>>> +\n>>> +\tif (GetTempPath((DWORD)length, buffer))\n>>> +\t\treturn 0;\n>>> +#endif\n>>> +\n>>> +\t/* This system doesn't like us, try to use the current directory */\n>>> +\tif (is_valid_tmp_path(\".\")) {\n>>> +\t\tstrncpy(buffer, \".\", length - 1);\n>>> +\t\tbuffer[length - 1] = '\\0';\n>>> +\t\treturn 0;\n>>> +\t}\n>>> +\n>>> +\treturn -1;\n>>> +}\n>>> +\n>>> +static void clar_unsandbox(void)\n>>> +{\n>>> +\tif (_clar_path[0] == '\\0')\n>>> +\t\treturn;\n>>> +\n>>> +\tcl_must_pass(chdir(\"..\"));\n>>> +\n>>> +\tfs_rm(_clar_path);\n>>> +}\n>>> +\n>>> +static int build_sandbox_path(void)\n>>> +{\n>>> +#ifdef CLAR_TMPDIR\n>>> +\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n>>> +#else\n>>> +\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n>>> +#endif\n>>> +\n>>> +\tsize_t len;\n>>> +\n>>> +\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n>>> +\t\treturn -1;\n>>> +\n>>> +\tlen = strlen(_clar_path);\n>>> +\n>>> +#ifdef _WIN32\n>>> +\t{ /* normalize path to POSIX forward slashes */\n>>> +\t\tsize_t i;\n>>> +\t\tfor (i = 0; i < len; ++i) {\n>>> +\t\t\tif (_clar_path[i] == '\\\\')\n>>> +\t\t\t\t_clar_path[i] = '/';\n>>> +\t\t}\n>>> +\t}\n>>> +#endif\n>>> +\n>>> +\tif (_clar_path[len - 1] != '/') {\n>>> +\t\t_clar_path[len++] = '/';\n>>> +\t}\n>>> +\n>>> +\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n>>> +\n>>> +#if defined(__MINGW32__)\n>>> +\tif (_mktemp(_clar_path) == NULL)\n>>> +\t\treturn -1;\n>>> +\n>>> +\tif (mkdir(_clar_path, 0700) != 0)\n>>> +\t\treturn -1;\n>>> +#elif defined(_WIN32)\n>>> +\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n>>> +\t\treturn -1;\n>>> +\n>>> +\tif (mkdir(_clar_path, 0700) != 0)\n>>> +\t\treturn -1;\n>>> +#else\n>>> +\tif (mkdtemp(_clar_path) == NULL)\n>>> +\t\treturn -1;\n>>> +#endif\n>>> +\n>>> +\treturn 0;\n>>> +}\n>>> +\n>>> +static int clar_sandbox(void)\n>>> +{\n>>> +\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n>>> +\t\treturn -1;\n>>> +\n>>> +\tif (chdir(_clar_path) != 0)\n>>> +\t\treturn -1;\n>>> +\n>>> +\treturn 0;\n>>> +}\n>>> +\n>>> +const char *clar_sandbox_path(void)\n>>> +{\n>>> +\treturn _clar_path;\n>>> +}\n>>> diff --git a/t/unit-tests/clar/clar/summary.h\n>>> b/t/unit-tests/clar/clar/summary.h\n>>> new file mode 100644\n>>> index 0000000000..4dd352e28b\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/clar/summary.h\n>>> @@ -0,0 +1,143 @@\n>>> +\n>>> +#include <stdio.h>\n>>> +#include <time.h>\n>>> +\n>>> +static int clar_summary_close_tag(\n>>> +    struct clar_summary *summary, const char *tag, int indent)\n>>> +{\n>>> +\tconst char *indt;\n>>> +\n>>> +\tif (indent == 0) indt = \"\";\n>>> +\telse if (indent == 1) indt = \"\\t\";\n>>> +\telse indt = \"\\t\\t\";\n>>> +\n>>> +\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n>>> +}\n>>> +\n>>> +static int clar_summary_testsuites(struct clar_summary *summary)\n>>> +{\n>>> +\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n>>> +}\n>>> +\n>>> +static int clar_summary_testsuite(struct clar_summary *summary,\n>>> +    int idn, const char *name, time_t timestamp,\n>>> +    int test_count, int fail_count, int error_count)\n>>> +{\n>>> +\tstruct tm *tm = localtime(&timestamp);\n>>> +\tchar iso_dt[20];\n>>> +\n>>> +\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n>>> +\t\treturn -1;\n>>> +\n>>> +\treturn fprintf(summary->fp, \"\\t<testsuite\"\n>>> +\t\t       \" id=\\\"%d\\\"\"\n>>> +\t\t       \" name=\\\"%s\\\"\"\n>>> +\t\t       \" hostname=\\\"localhost\\\"\"\n>>> +\t\t       \" timestamp=\\\"%s\\\"\"\n>>> +\t\t       \" tests=\\\"%d\\\"\"\n>>> +\t\t       \" failures=\\\"%d\\\"\"\n>>> +\t\t       \" errors=\\\"%d\\\">\\n\",\n>>> +\t\t       idn, name, iso_dt, test_count, fail_count,\n>>> error_count);\n>>> +}\n>>> +\n>>> +static int clar_summary_testcase(struct clar_summary *summary,\n>>> +    const char *name, const char *classname, double elapsed)\n>>> +{\n>>> +\treturn fprintf(summary->fp,\n>>> +\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n>>> +\t\tname, classname, elapsed);\n>>> +}\n>>> +\n>>> +static int clar_summary_failure(struct clar_summary *summary,\n>>> +    const char *type, const char *message, const char *desc)\n>>> +{\n>>> +\treturn fprintf(summary->fp,\n>>> +\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n>>> +\t    type, message, desc);\n>>> +}\n>>> +\n>>> +static int clar_summary_skipped(struct clar_summary *summary)\n>>> +{\n>>> +\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n>>> +}\n>>> +\n>>> +struct clar_summary *clar_summary_init(const char *filename)\n>>> +{\n>>> +\tstruct clar_summary *summary;\n>>> +\tFILE *fp;\n>>> +\n>>> +\tif ((fp = fopen(filename, \"w\")) == NULL) {\n>>> +\t\tperror(\"fopen\");\n>>> +\t\treturn NULL;\n>>> +\t}\n>>> +\n>>> +\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n>>> +\t\tperror(\"malloc\");\n>>> +\t\tfclose(fp);\n>>> +\t\treturn NULL;\n>>> +\t}\n>>> +\n>>> +\tsummary->filename = filename;\n>>> +\tsummary->fp = fp;\n>>> +\n>>> +\treturn summary;\n>>> +}\n>>> +\n>>> +int clar_summary_shutdown(struct clar_summary *summary)\n>>> +{\n>>> +\tstruct clar_report *report;\n>>> +\tconst char *last_suite = NULL;\n>>> +\n>>> +\tif (clar_summary_testsuites(summary) < 0)\n>>> +\t\tgoto on_error;\n>>> +\n>>> +\treport = _clar.reports;\n>>> +\twhile (report != NULL) {\n>>> +\t\tstruct clar_error *error = report->errors;\n>>> +\n>>> +\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) !=\n>>> 0) {\n>>> +\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n>>> +\t\t\t    report->start, _clar.tests_ran,\n>>> _clar.total_errors, 0) < 0)\n>>> +\t\t\t\tgoto on_error;\n>>> +\t\t}\n>>> +\n>>> +\t\tlast_suite = report->suite;\n>>> +\n>>> +\t\tclar_summary_testcase(summary, report->test, report->suite,\n>>> report->elapsed);\n>>> +\n>>> +\t\twhile (error != NULL) {\n>>> +\t\t\tif (clar_summary_failure(summary, \"assert\",\n>>> +\t\t\t    error->error_msg, error->description) < 0)\n>>> +\t\t\t\tgoto on_error;\n>>> +\n>>> +\t\t\terror = error->next;\n>>> +\t\t}\n>>> +\n>>> +\t\tif (report->status == CL_TEST_SKIP)\n>>> +\t\t\tclar_summary_skipped(summary);\n>>> +\n>>> +\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n>>> +\t\t\tgoto on_error;\n>>> +\n>>> +\t\treport = report->next;\n>>> +\n>>> +\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n>>> +\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) <\n>>> 0)\n>>> +\t\t\t\tgoto on_error;\n>>> +\t\t}\n>>> +\t}\n>>> +\n>>> +\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n>>> +\t    fclose(summary->fp) != 0)\n>>> +\t\tgoto on_error;\n>>> +\n>>> +\tprintf(\"written summary file to %s\\n\", summary->filename);\n>>> +\n>>> +\tfree(summary);\n>>> +\treturn 0;\n>>> +\n>>> +on_error:\n>>> +\tfclose(summary->fp);\n>>> +\tfree(summary);\n>>> +\treturn -1;\n>>> +}\n>>> diff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\n>>> new file mode 100755\n>>> index 0000000000..80996ac3e7\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/generate.py\n>>> @@ -0,0 +1,266 @@\n>>> +#!/usr/bin/env python\n>>> +#\n>>> +# Copyright (c) Vicent Marti. All rights reserved.\n>>> +#\n>>> +# This file is part of clar, distributed under the ISC license.\n>>> +# For full terms see the included COPYING file.\n>>> +#\n>>> +\n>>> +from __future__ import with_statement\n>>> +from string import Template\n>>> +import re, fnmatch, os, sys, codecs, pickle\n>>> +\n>>> +class Module(object):\n>>> +    class Template(object):\n>>> +        def __init__(self, module):\n>>> +            self.module = module\n>>> +\n>>> +        def _render_callback(self, cb):\n>>> +            if not cb:\n>>> +                return '    { NULL, NULL }'\n>>> +            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n>>> +\n>>> +    class DeclarationTemplate(Template):\n>>> +        def render(self):\n>>> +            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in\n>>> self.module.callbacks) + \"\\n\"\n>>> +\n>>> +            for initializer in self.module.initializers:\n>>> +                out += \"extern %s;\\n\" % initializer['declaration']\n>>> +\n>>> +            if self.module.cleanup:\n>>> +                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n>>> +\n>>> +            return out\n>>> +\n>>> +    class CallbacksTemplate(Template):\n>>> +        def render(self):\n>>> +            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" %\n>>> self.module.name\n>>> +            out += \",\\n\".join(self._render_callback(cb) for cb in\n>>> self.module.callbacks)\n>>> +            out += \"\\n};\\n\"\n>>> +            return out\n>>> +\n>>> +    class InfoTemplate(Template):\n>>> +        def render(self):\n>>> +            templates = []\n>>> +\n>>> +            initializers = self.module.initializers\n>>> +            if len(initializers) == 0:\n>>> +                initializers = [ None ]\n>>> +\n>>> +            for initializer in initializers:\n>>> +                name = self.module.clean_name()\n>>> +                if initializer and\n>>> initializer['short_name'].startswith('initialize_'):\n>>> +                    variant =\n>>> initializer['short_name'][len('initialize_'):]\n>>> +                    name += \" (%s)\" % variant.replace('_', ' ')\n>>> +\n>>> +                template = Template(\n>>> +            r\"\"\"\n>>> +    {\n>>> +        \"${clean_name}\",\n>>> +    ${initialize},\n>>> +    ${cleanup},\n>>> +        ${cb_ptr}, ${cb_count}, ${enabled}\n>>> +    }\"\"\"\n>>> +                ).substitute(\n>>> +                    clean_name = name,\n>>> +                    initialize = self._render_callback(initializer),\n>>> +                    cleanup = self._render_callback(self.module.cleanup),\n>>> +                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n>>> +                    cb_count = len(self.module.callbacks),\n>>> +                    enabled = int(self.module.enabled)\n>>> +                )\n>>> +                templates.append(template)\n>>> +\n>>> +            return ','.join(templates)\n>>> +\n>>> +    def __init__(self, name):\n>>> +        self.name = name\n>>> +\n>>> +        self.mtime = None\n>>> +        self.enabled = True\n>>> +        self.modified = False\n>>> +\n>>> +    def clean_name(self):\n>>> +        return self.name.replace(\"_\", \"::\")\n>>> +\n>>> +    def _skip_comments(self, text):\n>>> +        SKIP_COMMENTS_REGEX = re.compile(\n>>> +            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n>>> +            re.DOTALL | re.MULTILINE)\n>>> +\n>>> +        def _replacer(match):\n>>> +            s = match.group(0)\n>>> +            return \"\" if s.startswith('/') else s\n>>> +\n>>> +        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n>>> +\n>>> +    def parse(self, contents):\n>>> +        TEST_FUNC_REGEX =\n>>> r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n>>> +\n>>> +        contents = self._skip_comments(contents)\n>>> +        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n>>> +\n>>> +        self.callbacks = []\n>>> +        self.initializers = []\n>>> +        self.cleanup = None\n>>> +\n>>> +        for (declaration, symbol, short_name) in regex.findall(contents):\n>>> +            data = {\n>>> +                \"short_name\" : short_name,\n>>> +                \"declaration\" : declaration,\n>>> +                \"symbol\" : symbol\n>>> +            }\n>>> +\n>>> +            if short_name.startswith('initialize'):\n>>> +                self.initializers.append(data)\n>>> +            elif short_name == 'cleanup':\n>>> +                self.cleanup = data\n>>> +            else:\n>>> +                self.callbacks.append(data)\n>>> +\n>>> +        return self.callbacks != []\n>>> +\n>>> +    def refresh(self, path):\n>>> +        self.modified = False\n>>> +\n>>> +        try:\n>>> +            st = os.stat(path)\n>>> +\n>>> +            # Not modified\n>>> +            if st.st_mtime == self.mtime:\n>>> +                return True\n>>> +\n>>> +            self.modified = True\n>>> +            self.mtime = st.st_mtime\n>>> +\n>>> +            with codecs.open(path, encoding='utf-8') as fp:\n>>> +                raw_content = fp.read()\n>>> +\n>>> +        except IOError:\n>>> +            return False\n>>> +\n>>> +        return self.parse(raw_content)\n>>> +\n>>> +class TestSuite(object):\n>>> +\n>>> +    def __init__(self, path, output):\n>>> +        self.path = path\n>>> +        self.output = output\n>>> +\n>>> +    def should_generate(self, path):\n>>> +        if not os.path.isfile(path):\n>>> +            return True\n>>> +\n>>> +        if any(module.modified for module in self.modules.values()):\n>>> +            return True\n>>> +\n>>> +        return False\n>>> +\n>>> +    def find_modules(self):\n>>> +        modules = []\n>>> +        for root, _, files in os.walk(self.path):\n>>> +            module_root = root[len(self.path):]\n>>> +            module_root = [c for c in module_root.split(os.sep) if c]\n>>> +\n>>> +            tests_in_module = fnmatch.filter(files, \"*.c\")\n>>> +\n>>> +            for test_file in tests_in_module:\n>>> +                full_path = os.path.join(root, test_file)\n>>> +                module_name = \"_\".join(module_root +\n>>> [test_file[:-2]]).replace(\"-\", \"_\")\n>>> +\n>>> +                modules.append((full_path, module_name))\n>>> +\n>>> +        return modules\n>>> +\n>>> +    def load_cache(self):\n>>> +        path = os.path.join(self.output, '.clarcache')\n>>> +        cache = {}\n>>> +\n>>> +        try:\n>>> +            fp = open(path, 'rb')\n>>> +            cache = pickle.load(fp)\n>>> +            fp.close()\n>>> +        except (IOError, ValueError):\n>>> +            pass\n>>> +\n>>> +        return cache\n>>> +\n>>> +    def save_cache(self):\n>>> +        path = os.path.join(self.output, '.clarcache')\n>>> +        with open(path, 'wb') as cache:\n>>> +            pickle.dump(self.modules, cache)\n>>> +\n>>> +    def load(self, force = False):\n>>> +        module_data = self.find_modules()\n>>> +        self.modules = {} if force else self.load_cache()\n>>> +\n>>> +        for path, name in module_data:\n>>> +            if name not in self.modules:\n>>> +                self.modules[name] = Module(name)\n>>> +\n>>> +            if not self.modules[name].refresh(path):\n>>> +                del self.modules[name]\n>>> +\n>>> +    def disable(self, excluded):\n>>> +        for exclude in excluded:\n>>> +            for module in self.modules.values():\n>>> +                name = module.clean_name()\n>>> +                if name.startswith(exclude):\n>>> +                    module.enabled = False\n>>> +                    module.modified = True\n>>> +\n>>> +    def suite_count(self):\n>>> +        return sum(max(1, len(m.initializers)) for m in\n>>> self.modules.values())\n>>> +\n>>> +    def callback_count(self):\n>>> +        return sum(len(module.callbacks) for module in\n>>> self.modules.values())\n>>> +\n>>> +    def write(self):\n>>> +        output = os.path.join(self.output, 'clar.suite')\n>>> +\n>>> +        if not self.should_generate(output):\n>>> +            return False\n>>> +\n>>> +        with open(output, 'w') as data:\n>>> +            modules = sorted(self.modules.values(), key=lambda module:\n>>> module.name)\n>>> +\n>>> +            for module in modules:\n>>> +                t = Module.DeclarationTemplate(module)\n>>> +                data.write(t.render())\n>>> +\n>>> +            for module in modules:\n>>> +                t = Module.CallbacksTemplate(module)\n>>> +                data.write(t.render())\n>>> +\n>>> +            suites = \"static struct clar_suite _clar_suites[] = {\" +\n>>> ','.join(\n>>> +                Module.InfoTemplate(module).render() for module in modules\n>>> +            ) + \"\\n};\\n\"\n>>> +\n>>> +            data.write(suites)\n>>> +\n>>> +            data.write(\"static const size_t _clar_suite_count = %d;\\n\" %\n>>> self.suite_count())\n>>> +            data.write(\"static const size_t _clar_callback_count = %d;\\n\" %\n>>> self.callback_count())\n>>> +\n>>> +        self.save_cache()\n>>> +        return True\n>>> +\n>>> +if __name__ == '__main__':\n>>> +    from optparse import OptionParser\n>>> +\n>>> +    parser = OptionParser()\n>>> +    parser.add_option('-f', '--force', action=\"store_true\", dest='force',\n>>> default=False)\n>>> +    parser.add_option('-x', '--exclude', dest='excluded', action='append',\n>>> default=[])\n>>> +    parser.add_option('-o', '--output', dest='output')\n>>> +\n>>> +    options, args = parser.parse_args()\n>>> +    if len(args) > 1:\n>>> +        print(\"More than one path given\")\n>>> +        sys.exit(1)\n>>> +\n>>> +    path = args.pop() if args else '.'\n>>> +    output = options.output or path\n>>> +    suite = TestSuite(path, output)\n>>> +    suite.load(options.force)\n>>> +    suite.disable(options.excluded)\n>>> +    if suite.write():\n>>> +        print(\"Written `clar.suite` (%d tests in %d suites)\" %\n>>> (suite.callback_count(), suite.suite_count()))\n>>> diff --git a/t/unit-tests/clar/test/.gitignore\n>>> b/t/unit-tests/clar/test/.gitignore\n>>> new file mode 100644\n>>> index 0000000000..a477d0c40c\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/test/.gitignore\n>>> @@ -0,0 +1,4 @@\n>>> +clar.suite\n>>> +.clarcache\n>>> +clar_test\n>>> +*.o\n>>> diff --git a/t/unit-tests/clar/test/Makefile\n>>> b/t/unit-tests/clar/test/Makefile\n>>> new file mode 100644\n>>> index 0000000000..93c6b2ad32\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/test/Makefile\n>>> @@ -0,0 +1,39 @@\n>>> +#\n>>> +# Copyright (c) Vicent Marti. All rights reserved.\n>>> +#\n>>> +# This file is part of clar, distributed under the ISC license.\n>>> +# For full terms see the included COPYING file.\n>>> +#\n>>> +\n>>> +#\n>>> +# Set up the path to the clar sources and to the fixtures directory\n>>> +#\n>>> +# The fixture path needs to be an absolute path so it can be used\n>>> +# even after we have chdir'ed into the test directory while testing.\n>>> +#\n>>> +CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n>>> +TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n>>> +CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n>>> +CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n>>> +\n>>> +CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n>>> +\n>>> +.PHONY: clean\n>>> +\n>>> +# list the objects that go into our test\n>>> +objects = main.o sample.o\n>>> +\n>>> +# build the test executable itself\n>>> +clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n>>> +\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n>>> +\n>>> +# test object files depend on clar macros\n>>> +$(objects) : $(CLAR_PATH)clar.h\n>>> +\n>>> +# build the clar.suite file of test metadata\n>>> +clar.suite:\n>>> +\tpython \"$(CLAR_PATH)generate.py\" .\n>>> +\n>>> +# remove all generated files\n>>> +clean:\n>>> +\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\n>>> diff --git a/t/unit-tests/clar/test/clar_test.h\n>>> b/t/unit-tests/clar/test/clar_test.h\n>>> new file mode 100644\n>>> index 0000000000..0fcaa639aa\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/test/clar_test.h\n>>> @@ -0,0 +1,16 @@\n>>> +/*\n>>> + * Copyright (c) Vicent Marti. All rights reserved.\n>>> + *\n>>> + * This file is part of clar, distributed under the ISC license.\n>>> + * For full terms see the included COPYING file.\n>>> + */\n>>> +#ifndef __CLAR_TEST__\n>>> +#define __CLAR_TEST__\n>>> +\n>>> +/* Import the standard clar helper functions */\n>>> +#include \"clar.h\"\n>>> +\n>>> +/* Your custom shared includes / defines here */\n>>> +extern int global_test_counter;\n>>> +\n>>> +#endif\n>>> diff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\n>>> new file mode 100644\n>>> index 0000000000..59e56ad255\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/test/main.c\n>>> @@ -0,0 +1,40 @@\n>>> +/*\n>>> + * Copyright (c) Vicent Marti. All rights reserved.\n>>> + *\n>>> + * This file is part of clar, distributed under the ISC license.\n>>> + * For full terms see the included COPYING file.\n>>> + */\n>>> +\n>>> +#include \"clar_test.h\"\n>>> +\n>>> +/*\n>>> + * Sample main() for clar tests.\n>>> + *\n>>> + * You should write your own main routine for clar tests that does specific\n>>> + * setup and teardown as necessary for your application.  The only required\n>>> + * line is the call to `clar_test(argc, argv)`, which will execute the test\n>>> + * suite.  If you want to check the return value of the test application,\n>>> + * your main() should return the same value returned by clar_test().\n>>> + */\n>>> +\n>>> +int global_test_counter = 0;\n>>> +\n>>> +#ifdef _WIN32\n>>> +int __cdecl main(int argc, char *argv[])\n>>> +#else\n>>> +int main(int argc, char *argv[])\n>>> +#endif\n>>> +{\n>>> +\tint ret;\n>>> +\n>>> +\t/* Your custom initialization here */\n>>> +\tglobal_test_counter = 0;\n>>> +\n>>> +\t/* Run the test suite */\n>>> +\tret = clar_test(argc, argv);\n>>> +\n>>> +\t/* Your custom cleanup here */\n>>> +\tcl_assert_equal_i(8, global_test_counter);\n>>> +\n>>> +\treturn ret;\n>>> +}\n>>> diff --git a/t/unit-tests/clar/test/main.c.sample\n>>> b/t/unit-tests/clar/test/main.c.sample\n>>> new file mode 100644\n>>> index 0000000000..a4d91b72fa\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/test/main.c.sample\n>>> @@ -0,0 +1,27 @@\n>>> +/*\n>>> + * Copyright (c) Vicent Marti. All rights reserved.\n>>> + *\n>>> + * This file is part of clar, distributed under the ISC license.\n>>> + * For full terms see the included COPYING file.\n>>> + */\n>>> +\n>>> +#include \"clar_test.h\"\n>>> +\n>>> +/*\n>>> + * Minimal main() for clar tests.\n>>> + *\n>>> + * Modify this with any application specific setup or teardown that you\n>>> need.\n>>> + * The only required line is the call to `clar_test(argc, argv)`, which\n>>> will\n>>> + * execute the test suite.  If you want to check the return value of the\n>>> test\n>>> + * application, main() should return the same value returned by\n>>> clar_test().\n>>> + */\n>>> +\n>>> +#ifdef _WIN32\n>>> +int __cdecl main(int argc, char *argv[])\n>>> +#else\n>>> +int main(int argc, char *argv[])\n>>> +#endif\n>>> +{\n>>> +\t/* Run the test suite */\n>>> +\treturn clar_test(argc, argv);\n>>> +}\n>>> diff --git a/t/unit-tests/clar/test/resources/test/file\n>>> b/t/unit-tests/clar/test/resources/test/file\n>>> new file mode 100644\n>>> index 0000000000..220f4aa98a\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/test/resources/test/file\n>>> @@ -0,0 +1 @@\n>>> +File\n>>> diff --git a/t/unit-tests/clar/test/sample.c\n>>> b/t/unit-tests/clar/test/sample.c\n>>> new file mode 100644\n>>> index 0000000000..faa1209262\n>>> --- /dev/null\n>>> +++ b/t/unit-tests/clar/test/sample.c\n>>> @@ -0,0 +1,84 @@\n>>> +#include \"clar_test.h\"\n>>> +#include <sys/stat.h>\n>>> +\n>>> +static int file_size(const char *filename)\n>>> +{\n>>> +\tstruct stat st;\n>>> +\n>>> +\tif (stat(filename, &st) == 0)\n>>> +\t\treturn (int)st.st_size;\n>>> +\treturn -1;\n>>> +}\n>>> +\n>>> +void test_sample__initialize(void)\n>>> +{\n>>> +\tglobal_test_counter++;\n>>> +}\n>>> +\n>>> +void test_sample__cleanup(void)\n>>> +{\n>>> +\tcl_fixture_cleanup(\"test\");\n>>> +\n>>> +\tcl_assert(file_size(\"test/file\") == -1);\n>>> +}\n>>> +\n>>> +void test_sample__1(void)\n>>> +{\n>>> +\tcl_assert(1);\n>>> +\tcl_must_pass(0);  /* 0 == success */\n>>> +\tcl_must_fail(-1); /* <0 == failure */\n>>> +\tcl_must_pass(-1); /* demonstrate a failing call */\n>>> +}\n>>> +\n>>> +void test_sample__2(void)\n>>> +{\n>>> +\tcl_fixture_sandbox(\"test\");\n>>> +\n>>> +\tcl_assert(file_size(\"test/nonexistent\") == -1);\n>>> +\tcl_assert(file_size(\"test/file\") > 0);\n>>> +\tcl_assert(100 == 101);\n>>> +}\n>>> +\n>>> +void test_sample__strings(void)\n>>> +{\n>>> +\tconst char *actual = \"expected\";\n>>> +\tcl_assert_equal_s(\"expected\", actual);\n>>> +\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n>>> +\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n>>> +}\n>>> +\n>>> +void test_sample__strings_with_length(void)\n>>> +{\n>>> +\tconst char *actual = \"expected\";\n>>> +\tcl_assert_equal_strn(\"expected_\", actual, 8);\n>>> +\tcl_assert_equal_strn(\"exactly\", actual, 2);\n>>> +\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n>>> +\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n>>> +}\n>>> +\n>>> +void test_sample__int(void)\n>>> +{\n>>> +\tint value = 100;\n>>> +\tcl_assert_equal_i(100, value);\n>>> +\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n>>> +}\n>>> +\n>>> +void test_sample__int_fmt(void)\n>>> +{\n>>> +\tint value = 100;\n>>> +\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n>>> +}\n>>> +\n>>> +void test_sample__bool(void)\n>>> +{\n>>> +\tint value = 100;\n>>> +\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n>>> +\tcl_assert_equal_b(0, value);\n>>> +}\n>>> +\n>>> +void test_sample__ptr(void)\n>>> +{\n>>> +\tconst char *actual = \"expected\";\n>>> +\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n>>> +\tcl_assert_equal_p(&actual, actual);\n>>> +}\n>>\n> \n"},{"id":"501768","messageId":"Zs8uDnUAFFKF4DTF@tanuki","threadId":"61874","inReplyTo":"e9c015a6-7fc2-4783-9ef6-05359adf46cc@gmail.com","subject":"Re: [PATCH v6 00/13] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-08-28T14:03:02Z","receivedAt":"2024-08-28T14:03:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Aug 28, 2024 at 02:18:49PM +0100, Phillip Wood wrote:\n> On 20/08/2024 15:02, Patrick Steinhardt wrote:\n> > Hi,\n> > \n> > this is another version of my patch series that introduces the clar\n> > testing framework for our C unit tests.\n> \n> I've left some comments on the code, most of them are points I made in the\n> last round that received no response. My main concern is that the assertions\n> offered by clar are not as convinent as the check_* macros.\n\nDid you have a look at my reply at <ZsSTOczuGhT5zwoV@tanuki>, where I\nresponded to these concerns?\n\nIn summary: I'm aware that this is still a rough edge. I'd be happy to\nfollow up on this and improve usability of the assertions, but doing it\nlikely is a bit more involved, mostly because I want to upstream all\nchanges in this context. So I'd rather want to land a basic version\nfirst, and then I'd iterate and improve asserts.\n\n> What's the plan for converting our current tests if this gets merged? If we\n> were to add wrappers that provide check_int() etc. that would greatly\n> simplify the conversion. I think it would offer a more ergonomic api for\n> writing new tests than the verbose and non-typesafe cl_assert_equal_i() and\n> friends.\n\nMy plan would first be to let things cook for a bit while I sort out the\nrough spots upstream. Once done and once we are sufficiently sure that\nthis is the direction to go I'm happy to do the conversion myself.\n\nWhether we want to have wrappers... I dunno, I don't think the names are\nall that bad. They have a clear namespace and say rather directly what\nthey are doing, which I value more than briefness (to a certain extent,\nof course). The type safety is another topic though, and something I\nwill aim to address.\n\nThanks!\n\nPatrick\n"},{"id":"501772","messageId":"13c94c27-0975-46dc-b94e-72fd3972ef16@gmail.com","threadId":"61874","inReplyTo":"Zs8uDnUAFFKF4DTF@tanuki","subject":"Re: [PATCH v6 00/13] Introduce clar testing framework","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-28T14:58:50Z","receivedAt":"2024-08-28T14:58:53Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 28/08/2024 15:03, Patrick Steinhardt wrote:\n> On Wed, Aug 28, 2024 at 02:18:49PM +0100, Phillip Wood wrote:\n>> On 20/08/2024 15:02, Patrick Steinhardt wrote:\n>>> Hi,\n>>>\n>>> this is another version of my patch series that introduces the clar\n>>> testing framework for our C unit tests.\n>>\n>> I've left some comments on the code, most of them are points I made in the\n>> last round that received no response. My main concern is that the assertions\n>> offered by clar are not as convinent as the check_* macros.\n> \n> Did you have a look at my reply at <ZsSTOczuGhT5zwoV@tanuki>, where I\n> responded to these concerns?\n\nOh sorry I'd completely missed that mail (it came in while I was off \nline and I failed to notice it).\n\n> In summary: I'm aware that this is still a rough edge. I'd be happy to\n> follow up on this and improve usability of the assertions, but doing it\n> likely is a bit more involved, mostly because I want to upstream all\n> changes in this context. So I'd rather want to land a basic version\n> first, and then I'd iterate and improve asserts.\n\nThat sounds reasonable\n\n>> What's the plan for converting our current tests if this gets merged? If we\n>> were to add wrappers that provide check_int() etc. that would greatly\n>> simplify the conversion. I think it would offer a more ergonomic api for\n>> writing new tests than the verbose and non-typesafe cl_assert_equal_i() and\n>> friends.\n> \n> My plan would first be to let things cook for a bit while I sort out the\n> rough spots upstream. Once done and once we are sufficiently sure that\n> this is the direction to go I'm happy to do the conversion myself.\n> \n> Whether we want to have wrappers... I dunno, I don't think the names are\n> all that bad. They have a clear namespace and say rather directly what\n> they are doing, which I value more than briefness (to a certain extent,\n> of course).\n\nOne could argue the check_* are namespaced by \"check\". I find writing \nunit tests in C is pretty tedious and having to type cl_assert_equal_? \njust adds to that.\n\n> The type safety is another topic though, and something I\n> will aim to address.\n\nThanks, it sounds like we're more or less on the same page\n\nBest Wishes\n\nPhillip\n\n> Thanks!\n> \n> Patrick\n"},{"id":"501773","messageId":"f22dadee-5431-4184-9f92-9306e9241021@gmail.com","threadId":"61874","inReplyTo":"ZsSTOczuGhT5zwoV@tanuki","subject":"Re: [PATCH v5 0/9] Introduce clar testing framework","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-08-28T15:15:59Z","receivedAt":"2024-08-28T15:16:02Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 20/08/2024 13:59, Patrick Steinhardt wrote:\n> On Fri, Aug 16, 2024 at 02:37:34PM +0100, Phillip Wood wrote:\n>> Hi Patrick\n>>\n>> On 16/08/2024 08:04, Patrick Steinhardt wrote:\n>>\n>>   - As I think you've pointed out elsewhere there are no equivalents\n>>     for check_int(a, <|<=|>|>=, b) so we're forced to use cl_assert()\n>>     and forego the better diagnostic messages that come from a\n>>     dedicated comparison macro. We should fix this as a priority.\n> \n> Agreed, this one also feels rather limiting to me. Are you okay with me\n> doing this as a follow-up in case this series lands?\n\nYes\n\n>>   - cl_assert_equal_i() casts its arguments to int whereas check_int()\n>>     and check_uint() are careful to avoid truncation and keep the\n>>     original signedness (if that's a word). I think that's unlikely to\n>>     be a problem with our current test but could trip us up in the\n>>     future.\n> \n> Yeah. If it ever becomes a problem we can likely just introduce\n> something like `cl_assert_equal_u()` to achieve the same for unsigned.\n> Both should probably end up casting to `intmax_t` and `uintmax_t`,\n> respectively.\n\nSupporting wider arguments would make sense. At the moment \nclar__assert_equal() does not support PRIiMAX, only the non-standard PRIuZ.\n\n>>   - cl_assert_equal_s() prints each argument as-is. This means\n>>     that it passes NULL arguments through to snprintf() which is\n>>     undefined according to the C standard. Compare this to check_str()\n>>     that is NULL safe and is careful to escape control characters and\n>>     add delimiters to the beginning and end of the string to make it\n>>     obvious when a string contains leading or trailing whitespace.\n> \n> Good point indeed, and something I'm happy to fix upstream.\n\nThat's great\n\n>>   - The cl_assert_equal_?() macros lack type safety for the arguments\n>>     being compared as they are wrappers around a variadic function.\n>>     That could be fixed by having each macros wrap a dedicated\n>>     function that wraps clar__fail().\n> \n> Some of them do indeed, others generate issues. I don't think we have to\n> have dedicated functions, but could do something about this with\n> `__attribute__((format (printf, ...)))`.\n\nI wondered about suggesting '__attribute__((format (printf, ...)))' but \nwe'd need to double up the format argument in order to use it which is \nkind of messy. At the moment we pass \"%i\" with two integers.\n\n>>   - There is no equivalent of test_todo() to mark assertions that are\n>>     expected to fail. We're not using that yet in our tests but our\n>>     experience with the integration tests suggests that we are likely\n>>     to want this in the future.\n> \n> Heh, funny that you mention this. I had this discussion some 6 years ago\n> I think, where I also mentioned that this should exist as a feature. In\n> any case, I agree.\n\nExcellent!\n\n>>   - To me the \"sandbox\" feature is mis-named as it does not provide any\n>>     confinement. It is instead a useful mechanism for running a test in\n>>     a temporary directory created from a template.\n> \n> I guess we'll either just have to not use it or ignore that it's named a\n> bit awkwardly. Changing this in clar probably wouldn't work well because\n> other projects may depend on it.\n\nYes it's probably too late to rename it. I think being able to create a \ntest directory from a template directory could be useful, we just need \nto be mindful that the test code is not confined by a sandbox.\n\n>>   - There are no checks for failing memory allocations - the return\n>>     value of calloc() and strdup() are used without checking for NULL.\n> \n> I'll commit to fixing this upstream if this lands.\n\nGreat\n\n>>   - The use of longjmp is a bit of a double edged sword as it makes it\n>>     easy to leak resources on test failures.\n> \n> I have to say that this is one of the best features of the clar to me.\n> The current test framework we use doesn't, which in theory requires you\n> to always `return` whenever there was an error. But that results in code\n> that is both awful to read and write, so for most of the tests simply\n> don't bother at all. And consequently, the tests are quite likely to\n> cause segfaults once one of the checks fails because we didn't abort\n> running the testcase, but things are broken.\n\nI thought that the tests took care to bail out early where it made \nsense. Sometimes it is useful to continue for example when checking an \nstrbuf we might want to check alloc and len before bailing out. We're \nprobably not losing much by not doing that though.\n\n> In practice, I'd claim that you don't typically care all that much about\n> memory leaks once your basic assertions start to fail.\n\nI tend to agree. I was thinking more about exhausting fds and cleaning \nup files but that's probably not a big issue in practice.\n\n> So, things that need addressing and that I'm happy to do as follow ups:\n> \n>    - Introduce functions that compare integers.\n> \n>    - Improve type safety of the `cl_assert_equal_?()` macros.\n> \n>    - Adapt `cl_assert_equal_s()` to handle NULL pointers.\n> \n>    - Introduce checks for failing memory allocations.\n> \n> Nice to have would be support for known-failing tests.\n\nThis all sounds good to me\n\nSorry for missing this mail earlier.\n\nPhillip\n\n> Patrick\n"},{"id":"501999","messageId":"Zta-nU4UMyrWgABW@tanuki","threadId":"61874","inReplyTo":"c6f13f6b-7899-4bbd-986a-9bb1649b214f@gmail.com","subject":"Re: [PATCH v6 11/13] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T07:45:33Z","receivedAt":"2024-09-03T08:12:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Aug 28, 2024 at 02:17:05PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 20/08/2024 15:02, Patrick Steinhardt wrote:\n> > Convert the strvec tests to use the new clar unit testing framework.\n> > This is a first test balloon that demonstrates how the testing infra for\n> > clar-based tests looks like.\n> > \n> > The tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\n> > running that binary, it generates TAP output:\n> \n> It would be interesting to see a comparison between the current framework\n> and clar of the output from a failing test - the TAP output for passing\n> tests is pretty much the same regardless of the framework used.\n\nWill do.\n\n> >      # ./t/unit-tests/bin/unit-tests\n> >      TAP version 13\n> >      # start of suite 1: strvec\n> >      ok 1 - strvec::init\n> > [...] The binary also supports some parameters that allow us to run only\n> > a\n> > subset of unit tests or alter the output:\n> > \n> >      $ ./t/unit-tests/bin/unit-tests -h\n> >      Usage: ./t/unit-tests/bin/unit-tests [options]\n> > \n> >      Options:\n> >        -sname        Run only the suite with `name` (can go to individual test name)\n> >        -iname        Include the suite with `name`\n> >        -xname        Exclude the suite with `name`\n> >        -v            Increase verbosity (show suite names)\n> \n> The output above seems to include the suite name - are we running the tests\n> with '-v' from our Makefile?\n\nThe `-v` switch is actually doing nothing when generating TAP output.\n\n> >        -q            Only report tests that had an error\n> \n> This option is incompatible with TAP output. As we force TAP output we\n> should find a way to stop displaying this help.\n> \n> >        -Q            Quit as soon as a test fails\n> >        -t            Display results in tap format\n> \n> We force TAP output by adding '-t' to argv in main() so this line is not\n> very helpful\n\nTrue indeed. This is the default argument parsing and output from clar,\nso it's nothing that we can change. That being said, I guess the best\nway to address this is to use our own option parsing here instead of\nusing whatever clar provides, and then we can also print our own usage.\n\nWill amend accordingly.\n\n> >        -l            Print suite names\n> >        -r[filename]  Write summary file (to the optional filename)\n> \n> > diff --git a/t/unit-tests/strvec.c b/t/unit-tests/strvec.c\n> > [..]\n> > +#define check_strvec(vec, ...) \\\n> > +\tdo { \\\n> > +\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n> > +\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n> \n> As there are a lot occurrences of ARRAY_SIZE(expect) it is probably worth\n> adding\n> \n> \tsize_t expect_len = ARRAY_SIZE(expect);\n> \n> above.\n\nCan do.\n\n> > +\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n> > +\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n> > +\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n> \n> The conversion here loses the values of nr and alloc which is a shame as\n> they would be useful when debugging a test failure.\n\nThis is something I'd address in the future, once we have macros that\ncan do relative comparisons.\n\n> > +\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n> > +\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n> \n> The original test also printed the array index of the failing check. As the\n> elements of the test vectors all seem to be unique that is less of a worry\n> than if we had tests with repeating elements.\n> \n> > +\t} while (0)\n> > +\n> > +void test_strvec__init(void)\n> > +{\n> > +\tstruct strvec vec = STRVEC_INIT;\n> \n> If we're rewriting the tests perhaps we can take the opportunity to add a\n> blank line to each one after the variable declarations in accordance with\n> our coding guidelines.\n\nCan do.\n\n> It might be a good opportunity to show the set-up and tear-down facilities\n> in clar as well instead of repeating the initialization in each test.\n\nI don't think it's a good fit here, as setup and teardown would hit the\nsystem under test. I rather think they should be used in cases where you\ne.g. always have to setup a repository for your tests.\n\nPatrick\n"},{"id":"502000","messageId":"Zta-oVnKUNEE86q-@tanuki","threadId":"61874","inReplyTo":"2dbdb173-9e31-4dbc-a65a-dd952ec1c213@gmail.com","subject":"Re: [PATCH v6 12/13] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T07:45:37Z","receivedAt":"2024-09-03T08:12:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Aug 28, 2024 at 02:18:02PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 20/08/2024 15:02, Patrick Steinhardt wrote:\n> > Convert the ctype tests to use the new clar unit testing framework.\n> > Introduce a new function `cl_failf()`\n> \n> This is a nice addition which somewhat mitigates the lack of an equivalent\n> to test_msg() that adds addition context messages to test failures.\n> \n> > that allows us to print a\n> > formatted error message, which we can use to point out which of the\n> > characters was classified incorrectly. This results in output like this\n> > on failure:\n> > \n> >      # start of suite 1: ctype\n> >      ok 1 - ctype::isspace\n> >      not ok 2 - ctype::isdigit\n> >          ---\n> >          reason: |\n> >            Test failed.\n> \n> \"Test failed.\" is not the reason for the test failure\n\nThe clar expects two strings, one \"general\" description and the details.\nI agree that it's a bit shoddy to have this as part of the reason, it\nreally should be a separate \"field\". But it allows us to distinguish\ne.g. between test failures and tests which emitted a warning, only.\n\n> >            0x61 is classified incorrectly\n> >          at:\n> >            file: 't/unit-tests/ctype.c'\n> >            line: 38\n> >            function: 'test_ctype__isdigit'\n> >          ---\n> \n> This is rather verbose compared to the current framework\n\nI guess this is a matter of taste. I actually prefer the more verbose\nstyle.\n\n> # check \"isdigit(i) == !!memchr(\"123456789\", i, len)\" failed at\n> t/unit-tests/t-ctype.c:36\n> #    left: 1\n> #   right: 0\n> #       i: 0x30\n> not ok 2 - isdigit works\n> \n> The current tests also shows which characters are expected to return true\n> and distinguishes between the two possible failure modes which are (a)\n> misclassification and (b) returning a non-zero integer other than '1' as\n> \"true\". The new test output does not allow the person running the test to\n> distinguish between these two failure modes.\n\nThis one is true though. Will amend.\n\n> > diff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\n> > index 66ec2387cc6..4c461defe16 100644\n> > --- a/t/unit-tests/unit-test.h\n> > +++ b/t/unit-tests/unit-test.h\n> > @@ -1,3 +1,10 @@\n> >   #include \"git-compat-util.h\"\n> >   #include \"clar/clar.h\"\n> >   #include \"clar-decls.h\"\n> > +#include \"strbuf.h\"\n> > +\n> > +#define cl_failf(fmt, ...) do { \\\n> > +\tchar *desc = xstrfmt(fmt, __VA_ARGS__); \\\n> \n> In our current framework we avoid relying on the strbuf api and functions\n> like xstrfmt() that use it as we want to be able to test strbuf.c with the\n> framework. We'd be better with a macro wrapping a new function that uses a\n> stack allocated buffer and p_snprintf() like clar__assert_equal(). That\n> would allow us to upstream this change as well.\n\nI don't think it's all that important to avoid a low-level API like\n`xstrfmt()`. But the second argument makes sense to me indeed.\n\n> > +\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n> > +\tfree(desc); \\\n> \n> This is leaked on failure due to the use of longjmp.\n\nAs discussed elsewhere I don't think this leak matters all that much.\nBut it's getting fixed with your proposal, so that's that.\n\nPatrick\n"},{"id":"502001","messageId":"Zta-pKzaJtkedZTO@tanuki","threadId":"61874","inReplyTo":"435e3840-ee46-4ca3-a525-747848da9930@gmail.com","subject":"Re: [PATCH v6 02/13] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T07:45:40Z","receivedAt":"2024-09-03T08:12:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Aug 28, 2024 at 02:16:52PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 20/08/2024 15:02, Patrick Steinhardt wrote:\n> > Import the clar unit testing framework at commit 1516124 (Merge pull\n> > request #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\n> > will be wired up in subsequent commits.\n> \n> I would be good to explain why we're doing this so we have a record in the\n> project history. A summary of the advantages and disadvantages of using clar\n> vs our current unit test framework would be helpful as well.\n\nWill do.\n\n> > diff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\n> > [...]\n> > +static void\n> > +clar_parse_args(int argc, char **argv)\n> > +{\n> > [...]\n> > +\t\tcase 'q':\n> > +\t\t\t_clar.report_errors_only = 1;\n> > +\t\t\tbreak;\n> \n> I think this option is incompatible with TAP output as when TAP output is\n> selected the first error is printed as part of clar_print_ontest() and\n> clar_print_errors() is a no-op. We should error out if it is given with '-t'\n\nYup. I'll address this by implementing our own option parsing.\n\nThe remainder of your concerns relate to the clar implementation itself,\nand as such I won't yet address them. I'll work with upstream to get\nthese fixed after this series lands though, as outlined in [1].\n\n[1]: <ZsSTOczuGhT5zwoV@tanuki>\n\nPatrick\n"},{"id":"502004","messageId":"cover.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[PATCH v7 00/14] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:31Z","receivedAt":"2024-09-03T09:14:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the seventh version fo my patch series that introduces the clar\nunit testing framework.\n\nChanges compared to v6:\n\n  - Add some explanations for why we introduce clar in the first place\n    to the second commit message.\n\n  - Split out implementation of the test driver, that is the main\n    function, into a separate commit.\n\n  - Reimplement argument parsing ourselves. This allows more flexibility\n    and gets rid of some options that do not make sense for us.\n\n  - Add an empty line between declarations and code.\n\n  - Improve the test messages in ctype tests to also mention our\n    expectations.\n\n  - Adapt `cl_failf()` to not use `xstrfmt()`, but `snprintf()` with a\n    static buffer instead.\n\nThanks!\n\nPatrick\n\nJohannes Schindelin (4):\n  clar: avoid compile error with mingw-w64\n  clar(win32): avoid compile error due to unused `fs_copy()`\n  clar: stop including `shellapi.h` unnecessarily\n  clar: add CMake support\n\nPatrick Steinhardt (10):\n  t: do not pass GIT_TEST_OPTS to unit tests with prove\n  t: import the clar unit testing framework\n  t/clar: fix compatibility with NonStop\n  Makefile: fix sparse dependency on GENERATED_H\n  Makefile: make hdr-check depend on generated headers\n  Makefile: do not use sparse on third-party sources\n  Makefile: wire up the clar unit testing framework\n  t/unit-tests: implement test driver\n  t/unit-tests: convert strvec tests to use clar\n  t/unit-tests: convert ctype tests to use clar\n\n .gitignore                                 |   1 +\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |  53 +-\n contrib/buildsystems/CMakeLists.txt        |  53 ++\n t/Makefile                                 |   4 +-\n t/run-test.sh                              |   2 +-\n t/unit-tests/.gitignore                    |   2 +\n t/unit-tests/clar-generate.awk             |  50 ++\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 524 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n t/unit-tests/strvec.c                      | 241 ++++++\n t/unit-tests/t-strvec.c                    | 211 ------\n t/unit-tests/unit-test.c                   |  45 ++\n t/unit-tests/unit-test.h                   |  10 +\n 31 files changed, 3457 insertions(+), 234 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n rename t/unit-tests/{t-ctype.c => ctype.c} (68%)\n create mode 100644 t/unit-tests/strvec.c\n delete mode 100644 t/unit-tests/t-strvec.c\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\nRange-diff against v6:\n 1:  e48a6461137 =  1:  b67f10ec0b0 t: do not pass GIT_TEST_OPTS to unit tests with prove\n 2:  1710e9f9ff7 !  2:  55a9b46e65f t: import the clar unit testing framework\n    @@ Metadata\n      ## Commit message ##\n         t: import the clar unit testing framework\n     \n    +    Our unit testing framework is a homegrown solution. While it supports\n    +    most of our needs, it is likely that the volume of unit tests will grow\n    +    quite a bit in the future such that we can exercise low-level subsystems\n    +    directly. This surfaces several shortcomings that the current solution\n    +    has:\n    +\n    +      - There is no way to run only one specific tests. While some of our\n    +        unit tests wire this up manually, others don't. In general, it\n    +        requires quite a bit of boilerplate to get this set up correctly.\n    +\n    +      - Failures do not cause a test to stop execution directly. Instead,\n    +        the test author needs to return manually whenever an assertion\n    +        fails. This is rather verbose and is not done correctly in most of\n    +        our unit tests.\n    +\n    +      - Wiring up a new testcase requires both implementing the test\n    +        function and calling it in the respective test suite's main\n    +        function, which is creating code duplication.\n    +\n    +    We can of course fix all of these issues ourselves, but that feels\n    +    rather pointless when there are already so many unit testing frameworks\n    +    out there that have those features.\n    +\n    +    We line out some requirements for any unit testing framework in\n    +    \"Documentation/technical/unit-tests.txt\". The \"clar\" unit testing\n    +    framework, which isn't listed in that table yet, ticks many of the\n    +    boxes:\n    +\n    +      - It is licensed under ISC, which is compatible.\n    +\n    +      - It is easily vendorable because it is rather tiny at around 1200\n    +        lines of code.\n    +\n    +      - It is easily hackable due to the same reason.\n    +\n    +      - It has TAP support.\n    +\n    +      - It has skippable tests.\n    +\n    +      - It preprocesses test files in order to extract test functions, which\n    +        then get wired up automatically.\n    +\n    +    While it's not perfect, the fact that clar originates from the libgit2\n    +    project means that it should be rather easy for us to collaborate with\n    +    upstream to plug any gaps.\n    +\n         Import the clar unit testing framework at commit 1516124 (Merge pull\n         request #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\n         will be wired up in subsequent commits.\n 3:  5c21aa87aa2 =  3:  f24401f0a87 t/clar: fix compatibility with NonStop\n 4:  06d2bce0d82 =  4:  658a601c541 clar: avoid compile error with mingw-w64\n 5:  f88b3421a09 =  5:  0b8a6ac5fed clar(win32): avoid compile error due to unused `fs_copy()`\n 6:  5fb4c55be33 =  6:  c50e7a0ea68 clar: stop including `shellapi.h` unnecessarily\n 7:  e0dcbd5ca83 =  7:  b8f3f16dd27 Makefile: fix sparse dependency on GENERATED_H\n 8:  77a03f8df70 =  8:  3d3fe443b9a Makefile: make hdr-check depend on generated headers\n 9:  c91dd7327e3 =  9:  7d0f494850a Makefile: do not use sparse on third-party sources\n10:  115c15aa9ae ! 10:  9c74c5ae019 Makefile: wire up the clar unit testing framework\n    @@ Commit message\n         anything. Thus, it would cause a compiler error if a function name was\n         mistyped and thus not picked up by \"generate.py\".\n     \n    +    The test driver \"unit-test.c\" is an empty stub for now. It will get\n    +    implemented in the next commit.\n    +\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## .gitignore ##\n    @@ t/unit-tests/unit-test.c (new)\n     @@\n     +#include \"unit-test.h\"\n     +\n    -+int cmd_main(int argc, const char **argv)\n    ++int cmd_main(int argc UNUSED, const char **argv UNUSED)\n     +{\n    -+\tconst char **argv_copy;\n    -+\tint ret;\n    -+\n    -+\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n    -+\tALLOC_ARRAY(argv_copy, argc + 2);\n    -+\tCOPY_ARRAY(argv_copy, argv, argc);\n    -+\targv_copy[argc++] = \"-t\";\n    -+\targv_copy[argc] = NULL;\n    -+\n    -+\tret = clar_test(argc, (char **) argv_copy);\n    -+\n    -+\tfree(argv_copy);\n    -+\treturn ret;\n    ++\treturn 0;\n     +}\n     \n      ## t/unit-tests/unit-test.h (new) ##\n -:  ----------- > 11:  8bd5b3e2b29 t/unit-tests: implement test driver\n11:  b3b8df04872 ! 12:  3c3b9eacdfb t/unit-tests: convert strvec tests to use clar\n    @@ Commit message\n         clar-based tests looks like.\n     \n         The tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\n    -    running that binary, it generates TAP output:\n    +    running that binary with an injected error, it generates TAP output:\n     \n             # ./t/unit-tests/bin/unit-tests\n             TAP version 13\n    @@ Commit message\n             ok 1 - strvec::init\n             ok 2 - strvec::dynamic_init\n             ok 3 - strvec::clear\n    -        ok 4 - strvec::push\n    -        ok 5 - strvec::pushft_pushf\n    +        not ok 4 - strvec::push\n    +            ---\n    +            reason: |\n    +              String mismatch: (&vec)->v[i] != expect[i]\n    +              'foo' != 'fo' (at byte 2)\n    +            at:\n    +              file: 't/unit-tests/strvec.c'\n    +              line: 48\n    +              function: 'test_strvec__push'\n    +            ---\n    +        ok 5 - strvec::pushf\n             ok 6 - strvec::pushl\n             ok 7 - strvec::pushv\n             ok 8 - strvec::replace_at_head\n    @@ t/unit-tests/strvec.c (new)\n     +#define check_strvec(vec, ...) \\\n     +\tdo { \\\n     +\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n    -+\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n    -+\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n    -+\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n    ++\t\tsize_t expect_len = ARRAY_SIZE(expect); \\\n    ++\t\tcl_assert(expect_len > 0); \\\n    ++\t\tcl_assert_equal_p(expect[expect_len - 1], NULL); \\\n    ++\t\tcl_assert_equal_i((vec)->nr, expect_len - 1); \\\n     +\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n    -+\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n    ++\t\tfor (size_t i = 0; i < expect_len; i++) \\\n     +\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n     +\t} while (0)\n     +\n     +void test_strvec__init(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n     +\tcl_assert_equal_i(vec.nr, 0);\n     +\tcl_assert_equal_i(vec.alloc, 0);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__dynamic_init(void)\n     +{\n     +\tstruct strvec vec;\n    ++\n     +\tstrvec_init(&vec);\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n     +\tcl_assert_equal_i(vec.nr, 0);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__clear(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_push(&vec, \"foo\");\n     +\tstrvec_clear(&vec);\n     +\tcl_assert_equal_p(vec.v, empty_strvec);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__pushf(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushf(&vec, \"foo: %d\", 1);\n     +\tcheck_strvec(&vec, \"foo: 1\", NULL);\n     +\tstrvec_clear(&vec);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__pushl(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tstrvec_clear(&vec);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__replace_at_head(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tstrvec_replace(&vec, 0, \"replaced\");\n     +\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__replace_in_between(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tstrvec_replace(&vec, 1, \"replaced\");\n     +\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__replace_with_substring(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushl(&vec, \"foo\", NULL);\n     +\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n     +\tcheck_strvec(&vec, \"oo\", NULL);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__remove_at_head(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tstrvec_remove(&vec, 0);\n     +\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__remove_at_tail(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tstrvec_remove(&vec, 2);\n     +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__remove_in_between(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tstrvec_remove(&vec, 1);\n     +\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__pop_empty_array(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pop(&vec);\n     +\tcheck_strvec(&vec, NULL);\n     +\tstrvec_clear(&vec);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__pop_non_empty_array(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tstrvec_pop(&vec);\n     +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__split_empty_string(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_split(&vec, \"\");\n     +\tcheck_strvec(&vec, NULL);\n     +\tstrvec_clear(&vec);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__split_single_item(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_split(&vec, \"foo\");\n     +\tcheck_strvec(&vec, \"foo\", NULL);\n     +\tstrvec_clear(&vec);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__split_multiple_items(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_split(&vec, \"foo bar baz\");\n     +\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n     +\tstrvec_clear(&vec);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__split_whitespace_only(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_split(&vec, \" \\t\\n\");\n     +\tcheck_strvec(&vec, NULL);\n     +\tstrvec_clear(&vec);\n    @@ t/unit-tests/strvec.c (new)\n     +void test_strvec__split_multiple_consecutive_whitespaces(void)\n     +{\n     +\tstruct strvec vec = STRVEC_INIT;\n    ++\n     +\tstrvec_split(&vec, \"foo\\n\\t bar\");\n     +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n     +\tstrvec_clear(&vec);\n12:  1ac2e48a7f2 ! 13:  c8360db2f86 t/unit-tests: convert ctype tests to use clar\n    @@ Commit message\n         on failure:\n     \n             # start of suite 1: ctype\n    -        ok 1 - ctype::isspace\n    -        not ok 2 - ctype::isdigit\n    +        not ok 1 - ctype::isspace\n                 ---\n                 reason: |\n                   Test failed.\n    -              0x61 is classified incorrectly\n    +              0x0d is classified incorrectly: expected 0, got 1\n                 at:\n                   file: 't/unit-tests/ctype.c'\n    -              line: 38\n    -              function: 'test_ctype__isdigit'\n    +              line: 36\n    +              function: 'test_ctype__isspace'\n                 ---\n    +        ok 2 - ctype::isdigit\n             ok 3 - ctype::isalpha\n             ok 4 - ctype::isalnum\n             ok 5 - ctype::is_glob_special\n    @@ t/unit-tests/t-ctype.c => t/unit-tests/ctype.c\n     -\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n     -\t\t} \\\n     -\t\tcheck(!class(EOF)); \\\n    --\t} \\\n    -+\tfor (int i = 0; i < 256; i++) \\\n    -+\t\tif (class(i) != !!memchr(string, i, len)) \\\n    -+\t\t\tcl_failf(\"0x%02x is classified incorrectly\", i); \\\n    ++\tfor (int i = 0; i < 256; i++) { \\\n    ++\t\tint actual = class(i), expect = !!memchr(string, i, len); \\\n    ++\t\tif (actual != expect) \\\n    ++\t\t\tcl_failf(\"0x%02x is classified incorrectly: expected %d, got %d\", \\\n    ++\t\t\t\t i, expect, actual); \\\n    + \t} \\\n     +\tcl_assert(!class(EOF)); \\\n      } while (0)\n      \n    @@ t/unit-tests/unit-test.h\n     +#include \"strbuf.h\"\n     +\n     +#define cl_failf(fmt, ...) do { \\\n    -+\tchar *desc = xstrfmt(fmt, __VA_ARGS__); \\\n    ++\tchar desc[4096]; \\\n    ++\tsnprintf(desc, sizeof(desc), fmt, __VA_ARGS__); \\\n     +\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n    -+\tfree(desc); \\\n     +} while (0)\n13:  131036c398e = 14:  d51c146cd9d clar: add CMake support\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502005","messageId":"b67f10ec0b08699eec93e3a7eb6ef3e06b5cedba.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 01/14] t: do not pass GIT_TEST_OPTS to unit tests with prove","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:34Z","receivedAt":"2024-09-03T09:14:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When using the prove target, we append GIT_TEST_OPTS to the arguments\nthat we execute each of the tests with. This doesn't only include the\nintended test scripts, but also ends up passing the arguments to our\nunit tests. This is unintentional though as they do not even know to\ninterpret those arguments, and is inconsistent with how we execute unit\ntests without prove.\n\nThis isn't much of an issue because our current set of unit tests mostly\nignore their arguments anyway. With the introduction of clar-based unit\ntests this is about to become an issue though, as these do parse their\ncommand line argument to alter behaviour.\n\nPrepare for this by passing GIT_TEST_OPTS to \"run-test.sh\" via an\nenvironment variable. Like this, we can conditionally forward it to our\ntest scripts, only.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/Makefile    | 3 ++-\n t/run-test.sh | 2 +-\n 2 files changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/Makefile b/t/Makefile\nindex 4c30e7c06fb..d2212de0b78 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -68,7 +68,8 @@ failed:\n \ttest -z \"$$failed\" || $(MAKE) $$failed\n \n prove: pre-clean check-chainlint $(TEST_LINT)\n-\t@echo \"*** prove (shell & unit tests) ***\"; $(CHAINLINTSUPPRESS) TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS) :: $(GIT_TEST_OPTS)\n+\t@echo \"*** prove (shell & unit tests) ***\"\n+\t@$(CHAINLINTSUPPRESS) TEST_OPTIONS='$(GIT_TEST_OPTS)' TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS)\n \t$(MAKE) clean-except-prove-cache\n \n $(T):\ndiff --git a/t/run-test.sh b/t/run-test.sh\nindex 13c353b91b4..63328ac630c 100755\n--- a/t/run-test.sh\n+++ b/t/run-test.sh\n@@ -10,7 +10,7 @@ case \"$1\" in\n \t\techo >&2 \"ERROR: TEST_SHELL_PATH is empty or not set\"\n \t\texit 1\n \tfi\n-\texec \"${TEST_SHELL_PATH}\" \"$@\"\n+\texec \"${TEST_SHELL_PATH}\" \"$@\" ${TEST_OPTIONS}\n \t;;\n *)\n \texec \"$@\"\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502007","messageId":"55a9b46e65f2072ca7994b042dfe24d1c153c334.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 02/14] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:37Z","receivedAt":"2024-09-03T09:14:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Our unit testing framework is a homegrown solution. While it supports\nmost of our needs, it is likely that the volume of unit tests will grow\nquite a bit in the future such that we can exercise low-level subsystems\ndirectly. This surfaces several shortcomings that the current solution\nhas:\n\n  - There is no way to run only one specific tests. While some of our\n    unit tests wire this up manually, others don't. In general, it\n    requires quite a bit of boilerplate to get this set up correctly.\n\n  - Failures do not cause a test to stop execution directly. Instead,\n    the test author needs to return manually whenever an assertion\n    fails. This is rather verbose and is not done correctly in most of\n    our unit tests.\n\n  - Wiring up a new testcase requires both implementing the test\n    function and calling it in the respective test suite's main\n    function, which is creating code duplication.\n\nWe can of course fix all of these issues ourselves, but that feels\nrather pointless when there are already so many unit testing frameworks\nout there that have those features.\n\nWe line out some requirements for any unit testing framework in\n\"Documentation/technical/unit-tests.txt\". The \"clar\" unit testing\nframework, which isn't listed in that table yet, ticks many of the\nboxes:\n\n  - It is licensed under ISC, which is compatible.\n\n  - It is easily vendorable because it is rather tiny at around 1200\n    lines of code.\n\n  - It is easily hackable due to the same reason.\n\n  - It has TAP support.\n\n  - It has skippable tests.\n\n  - It preprocesses test files in order to extract test functions, which\n    then get wired up automatically.\n\nWhile it's not perfect, the fact that clar originates from the libgit2\nproject means that it should be rather easy for us to collaborate with\nupstream to plug any gaps.\n\nImport the clar unit testing framework at commit 1516124 (Merge pull\nrequest #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |   4 +-\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 153 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n 20 files changed, 2943 insertions(+), 1 deletion(-)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n\ndiff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt\nindex 206037ffb19..5a432b7b29c 100644\n--- a/Documentation/technical/unit-tests.txt\n+++ b/Documentation/technical/unit-tests.txt\n@@ -203,6 +203,7 @@ GitHub / GitLab stars to estimate this.\n :criterion: https://github.com/Snaipe/Criterion[Criterion]\n :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n :check: https://libcheck.github.io/check/[Check]\n+:clar: https://github.com/clar-test/clar[Clar]\n \n [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n |=====\n@@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n+{clar},{isc},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n |=====\n \n === Additional framework candidates\ndiff --git a/Makefile b/Makefile\nindex a87e18b317d..28742a60964 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1331,6 +1331,8 @@ THIRD_PARTY_SOURCES += compat/poll/%\n THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n@@ -3261,7 +3263,7 @@ $(SP_OBJ): %.sp: %.c %.o\n .PHONY: sparse\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n ifndef OPENSSL_SHA1\n \tEXCEPT_HDRS += sha1/openssl.h\n endif\ndiff --git a/t/unit-tests/clar/.github/workflows/ci.yml b/t/unit-tests/clar/.github/workflows/ci.yml\nnew file mode 100644\nindex 00000000000..b1ac2de460a\n--- /dev/null\n+++ b/t/unit-tests/clar/.github/workflows/ci.yml\n@@ -0,0 +1,23 @@\n+name: CI\n+\n+on:\n+  push:\n+    branches: [ main ]\n+  pull_request:\n+    branches: [ main ]\n+\n+jobs:\n+  build:\n+    strategy:\n+      matrix:\n+        os: [ ubuntu-latest, macos-latest ]\n+\n+    runs-on: ${{ matrix.os }}\n+\n+    steps:\n+    - name: Check out\n+      uses: actions/checkout@v2\n+    - name: Build\n+      run: |\n+        cd test\n+        make\ndiff --git a/t/unit-tests/clar/COPYING b/t/unit-tests/clar/COPYING\nnew file mode 100644\nindex 00000000000..8983817f0c9\n--- /dev/null\n+++ b/t/unit-tests/clar/COPYING\n@@ -0,0 +1,15 @@\n+ISC License\n+\n+Copyright (c) 2011-2015 Vicent Marti\n+\n+Permission to use, copy, modify, and/or distribute this software for any\n+purpose with or without fee is hereby granted, provided that the above\n+copyright notice and this permission notice appear in all copies.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ndiff --git a/t/unit-tests/clar/README.md b/t/unit-tests/clar/README.md\nnew file mode 100644\nindex 00000000000..a8961c5f10f\n--- /dev/null\n+++ b/t/unit-tests/clar/README.md\n@@ -0,0 +1,329 @@\n+Come out and Clar\n+=================\n+\n+In Catalan, \"clar\" means clear, easy to perceive.  Using clar will make it\n+easy to test and make clear the quality of your code.\n+\n+> _Historical note_\n+>\n+> Originally the clar project was named \"clay\" because the word \"test\" has its\n+> roots in the latin word *\"testum\"*, meaning \"earthen pot\", and *\"testa\"*,\n+> meaning \"piece of burned clay\"?\n+>\n+> This is because historically, testing implied melting metal in a pot to\n+> check its quality.  Clay is what tests are made of.\n+\n+## Quick Usage Overview\n+\n+Clar is a minimal C unit testing framework. It's been written to replace the\n+old framework in [libgit2][libgit2], but it's both very versatile and\n+straightforward to use.\n+\n+Can you count to funk?\n+\n+- **Zero: Initialize test directory**\n+\n+    ~~~~ sh\n+    $ mkdir tests\n+    $ cp -r $CLAR_ROOT/clar* tests\n+    $ cp $CLAR_ROOT/test/clar_test.h tests\n+    $ cp $CLAR_ROOT/test/main.c.sample tests/main.c\n+    ~~~~\n+\n+- **One: Write some tests**\n+\n+    File: tests/adding.c:\n+\n+    ~~~~ c\n+    /* adding.c for the \"Adding\" suite */\n+    #include \"clar.h\"\n+\n+    static int *answer;\n+\n+    void test_adding__initialize(void)\n+    {\n+        answer = malloc(sizeof(int));\n+        cl_assert_(answer != NULL, \"No memory left?\");\n+        *answer = 42;\n+    }\n+\n+    void test_adding__cleanup(void)\n+    {\n+        free(answer);\n+    }\n+\n+    void test_adding__make_sure_math_still_works(void)\n+    {\n+        cl_assert_(5 > 3, \"Five should probably be greater than three\");\n+        cl_assert_(-5 < 2, \"Negative numbers are small, I think\");\n+        cl_assert_(*answer == 42, \"The universe is doing OK. And the initializer too.\");\n+    }\n+    ~~~~~\n+\n+- **Two: Build the test executable**\n+\n+    ~~~~ sh\n+    $ cd tests\n+    $ $CLAR_PATH/generate.py .\n+    Written `clar.suite` (1 suites)\n+    $ gcc -I. clar.c main.c adding.c -o testit\n+    ~~~~\n+\n+- **Funk: Funk it.**\n+\n+    ~~~~ sh\n+    $ ./testit\n+    ~~~~\n+\n+## The Clar Test Suite\n+\n+Writing a test suite is pretty straightforward. Each test suite is a `*.c`\n+file with a descriptive name: this encourages modularity.\n+\n+Each test suite has optional initialize and cleanup methods. These methods\n+will be called before and after running **each** test in the suite, even if\n+such test fails. As a rule of thumb, if a test needs a different initializer\n+or cleanup method than another test in the same module, that means it\n+doesn't belong in that module. Keep that in mind when grouping tests\n+together.\n+\n+The `initialize` and `cleanup` methods have the following syntax, with\n+`suitename` being the current suite name, e.g. `adding` for the `adding.c`\n+suite.\n+\n+~~~~ c\n+void test_suitename__initialize(void)\n+{\n+    /* init */\n+}\n+\n+void test_suitename__cleanup(void)\n+{\n+    /* cleanup */\n+}\n+~~~~\n+\n+These methods are encouraged to use static, global variables to store the state\n+that will be used by all tests inside the suite.\n+\n+~~~~ c\n+static git_repository *_repository;\n+\n+void test_status__initialize(void)\n+{\n+    create_tmp_repo(STATUS_REPO);\n+    git_repository_open(_repository, STATUS_REPO);\n+}\n+\n+void test_status__cleanup(void)\n+{\n+    git_repository_close(_repository);\n+    git_path_rm(STATUS_REPO);\n+}\n+\n+void test_status__simple_test(void)\n+{\n+    /* do something with _repository */\n+}\n+~~~~\n+\n+Writing the actual tests is just as straightforward. Tests have the\n+`void test_suitename__test_name(void)` signature, and they should **not**\n+be static. Clar will automatically detect and list them.\n+\n+Tests are run as they appear on their original suites: they have no return\n+value. A test is considered \"passed\" if it doesn't raise any errors. Check\n+the \"Clar API\" section to see the various helper functions to check and\n+raise errors during test execution.\n+\n+__Caution:__ If you use assertions inside of `test_suitename__initialize`,\n+make sure that you do not rely on `__initialize` being completely run\n+inside your `test_suitename__cleanup` function. Otherwise you might\n+encounter ressource cleanup twice.\n+\n+## How does Clar work?\n+\n+To use Clar:\n+\n+1. copy the Clar boilerplate to your test directory\n+2. copy (and probably modify) the sample `main.c` (from\n+   `$CLAR_PATH/test/main.c.sample`)\n+3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and\n+   write out the test suite metadata.\n+4. compile your test files and the Clar boilerplate into a single test\n+   executable\n+5. run the executable to test!\n+\n+The Clar boilerplate gives you a set of useful test assertions and features\n+(like accessing or making sandbox copies of fixture data).  It consists of\n+the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory.\n+You should not need to edit these files.\n+\n+The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes\n+`clar_test(argc, argv)` to run the tests.  Usually, you will edit this file\n+to perform any framework specific initialization and teardown that you need.\n+\n+The Clar mixer (`generate.py`) recursively scans your test directory for\n+any `.c` files, parses them, and writes the `clar.suite` file with all of\n+the metadata about your tests.  When you build, the `clar.suite` file is\n+included into `clar.c`.\n+\n+The mixer can be run with **Python 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and PyPy 1.6**.\n+\n+Commandline usage of the mixer is as follows:\n+\n+    $ ./generate.py .\n+\n+Where `.` is the folder where all the test suites can be found. The mixer\n+will automatically locate all the relevant source files and build the\n+testing metadata. The metadata will be written to `clar.suite`, in the same\n+folder as all the test suites. This file is included by `clar.c` and so\n+must be accessible via `#include` when building the test executable.\n+\n+    $ gcc -I. clar.c main.c suite1.c test2.c -o run_tests\n+\n+**Note that the Clar mixer only needs to be ran when adding new tests to a\n+suite, in order to regenerate the metadata**. As a result, the `clar.suite`\n+file can be checked into version control if you wish to be able to build\n+your test suite without having to re-run the mixer.\n+\n+This is handy when e.g. generating tests in a local computer, and then\n+building and testing them on an embedded device or a platform where Python\n+is not available.\n+\n+### Fixtures\n+\n+Clar can create sandboxed fixtures for you to use in your test. You'll need to compile *clar.c* with an additional `CFLAG`, `-DCLAR_FIXTURE_PATH`. This should be an absolute path to your fixtures directory.\n+\n+Once that's done, you can use the fixture API as defined below.\n+\n+## The Clar API\n+\n+Clar makes the following methods available from all functions in a test\n+suite.\n+\n+-   `cl_must_pass(call)`, `cl_must_pass_(call, message)`: Verify that the given\n+    function call passes, in the POSIX sense (returns a value greater or equal\n+    to 0).\n+\n+-   `cl_must_fail(call)`, `cl_must_fail_(call, message)`: Verify that the given\n+    function call fails, in the POSIX sense (returns a value less than 0).\n+\n+-   `cl_assert(expr)`, `cl_assert_(expr, message)`: Verify that `expr` is true.\n+\n+-   `cl_check_pass(call)`, `cl_check_pass_(call, message)`: Verify that the\n+    given function call passes, in the POSIX sense (returns a value greater or\n+    equal to 0). If the function call doesn't succeed, a test failure will be\n+    logged but the test's execution will continue.\n+\n+-   `cl_check_fail(call)`, `cl_check_fail_(call, message)`: Verify that the\n+    given function call fails, in the POSIX sense (returns a value less than\n+    0). If the function call doesn't fail, a test failure will be logged but\n+    the test's execution will continue.\n+\n+-   `cl_check(expr)`: Verify that `expr` is true. If `expr` is not\n+    true, a test failure will be logged but the test's execution will continue.\n+\n+-   `cl_fail(message)`: Fail the current test with the given message.\n+\n+-   `cl_warning(message)`: Issue a warning. This warning will be\n+    logged as a test failure but the test's execution will continue.\n+\n+-   `cl_set_cleanup(void (*cleanup)(void *), void *opaque)`: Set the cleanup\n+    method for a single test. This method will be called with `opaque` as its\n+    argument before the test returns (even if the test has failed).\n+    If a global cleanup method is also available, the local cleanup will be\n+    called first, and then the global.\n+\n+-   `cl_assert_equal_i(int,int)`: Verify that two integer values are equal.\n+    The advantage of this over a simple `cl_assert` is that it will format\n+    a much nicer error report if the values are not equal.\n+\n+-   `cl_assert_equal_s(const char *,const char *)`: Verify that two strings\n+    are equal.  The expected value can also be NULL and this will correctly\n+    test for that.\n+\n+-   `cl_fixture_sandbox(const char *)`: Sets up a sandbox for a fixture\n+    so that you can mutate the file directly.\n+\n+-   `cl_fixture_cleanup(const char *)`: Tears down the previous fixture\n+    sandbox.\n+\n+-   `cl_fixture(const char *)`: Gets the full path to a fixture file.\n+\n+Please do note that these methods are *always* available whilst running a\n+test, even when calling auxiliary/static functions inside the same file.\n+\n+It's strongly encouraged to perform test assertions in auxiliary methods,\n+instead of returning error values. This is considered good Clar style.\n+\n+Style Example:\n+\n+~~~~ c\n+/*\n+ * Bad style: auxiliary functions return an error code\n+ */\n+\n+static int check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    if (aux == NULL)\n+        return -1;\n+\n+    return strcmp(my_function(aux), str) == 0 ? 0 : -1;\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    cl_must_pass_(\n+        check_string(\"foo\"),\n+        \"String differs after processing\"\n+    );\n+\n+    cl_must_pass_(\n+        check_string(\"bar\"),\n+        \"String differs after processing\"\n+    );\n+}\n+~~~~\n+\n+~~~~ c\n+/*\n+ * Good style: auxiliary functions perform assertions\n+ */\n+\n+static void check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    cl_assert_(\n+        aux != NULL,\n+        \"String processing failed\"\n+    );\n+\n+    cl_assert_(\n+        strcmp(my_function(aux), str) == 0,\n+        \"String differs after processing\"\n+    );\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    check_string(\"foo\");\n+    check_string(\"bar\");\n+}\n+~~~~\n+\n+About Clar\n+==========\n+\n+Clar has been written from scratch by [Vicent Martí](https://github.com/vmg),\n+to replace the old testing framework in [libgit2][libgit2].\n+\n+Do you know what languages are *in* on the SF startup scene? Node.js *and*\n+Latin.  Follow [@vmg](https://www.twitter.com/vmg) on Twitter to\n+receive more lessons on word etymology. You can be hip too.\n+\n+\n+[libgit2]: https://github.com/libgit2/libgit2\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nnew file mode 100644\nindex 00000000000..3fc2c768158\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.c\n@@ -0,0 +1,842 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#include <assert.h>\n+#include <setjmp.h>\n+#include <stdlib.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <math.h>\n+#include <stdarg.h>\n+#include <wchar.h>\n+#include <time.h>\n+\n+/* required for sandboxing */\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#ifdef _WIN32\n+#\tinclude <windows.h>\n+#\tinclude <io.h>\n+#\tinclude <shellapi.h>\n+#\tinclude <direct.h>\n+\n+#\tdefine _MAIN_CC __cdecl\n+\n+#\tifndef stat\n+#\t\tdefine stat(path, st) _stat(path, st)\n+#\tendif\n+#\tifndef mkdir\n+#\t\tdefine mkdir(path, mode) _mkdir(path)\n+#\tendif\n+#\tifndef chdir\n+#\t\tdefine chdir(path) _chdir(path)\n+#\tendif\n+#\tifndef access\n+#\t\tdefine access(path, mode) _access(path, mode)\n+#\tendif\n+#\tifndef strdup\n+#\t\tdefine strdup(str) _strdup(str)\n+#\tendif\n+#\tifndef strcasecmp\n+#\t\tdefine strcasecmp(a,b) _stricmp(a,b)\n+#\tendif\n+\n+#\tifndef __MINGW32__\n+#\t\tpragma comment(lib, \"shell32\")\n+#\t\tifndef strncpy\n+#\t\t\tdefine strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)\n+#\t\tendif\n+#\t\tifndef W_OK\n+#\t\t\tdefine W_OK 02\n+#\t\tendif\n+#\t\tifndef S_ISDIR\n+#\t\t\tdefine S_ISDIR(x) ((x & _S_IFDIR) != 0)\n+#\t\tendif\n+#\t\tdefine p_snprintf(buf,sz,fmt,...) _snprintf_s(buf,sz,_TRUNCATE,fmt,__VA_ARGS__)\n+#\telse\n+#\t\tdefine p_snprintf snprintf\n+#\tendif\n+\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"Iu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"Ix\"\n+#\tendif\n+\n+#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+\ttypedef struct stat STAT_T;\n+#\telse\n+\ttypedef struct _stat STAT_T;\n+#\tendif\n+#else\n+#\tinclude <sys/wait.h> /* waitpid(2) */\n+#\tinclude <unistd.h>\n+#\tdefine _MAIN_CC\n+#\tdefine p_snprintf snprintf\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"zu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"zx\"\n+#\tendif\n+\ttypedef struct stat STAT_T;\n+#endif\n+\n+#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n+\n+#include \"clar.h\"\n+\n+static void fs_rm(const char *_source);\n+static void fs_copy(const char *_source, const char *dest);\n+\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name);\n+#endif\n+\n+struct clar_error {\n+\tconst char *file;\n+\tconst char *function;\n+\tsize_t line_number;\n+\tconst char *error_msg;\n+\tchar *description;\n+\n+\tstruct clar_error *next;\n+};\n+\n+struct clar_explicit {\n+\tsize_t suite_idx;\n+\tconst char *filter;\n+\n+\tstruct clar_explicit *next;\n+};\n+\n+struct clar_report {\n+\tconst char *test;\n+\tint test_number;\n+\tconst char *suite;\n+\n+\tenum cl_test_status status;\n+\ttime_t start;\n+\tdouble elapsed;\n+\n+\tstruct clar_error *errors;\n+\tstruct clar_error *last_error;\n+\n+\tstruct clar_report *next;\n+};\n+\n+struct clar_summary {\n+\tconst char *filename;\n+\tFILE *fp;\n+};\n+\n+static struct {\n+\tenum cl_test_status test_status;\n+\n+\tconst char *active_test;\n+\tconst char *active_suite;\n+\n+\tint total_skipped;\n+\tint total_errors;\n+\n+\tint tests_ran;\n+\tint suites_ran;\n+\n+\tenum cl_output_format output_format;\n+\n+\tint report_errors_only;\n+\tint exit_on_error;\n+\tint verbosity;\n+\n+\tint write_summary;\n+\tchar *summary_filename;\n+\tstruct clar_summary *summary;\n+\n+\tstruct clar_explicit *explicit;\n+\tstruct clar_explicit *last_explicit;\n+\n+\tstruct clar_report *reports;\n+\tstruct clar_report *last_report;\n+\n+\tvoid (*local_cleanup)(void *);\n+\tvoid *local_cleanup_payload;\n+\n+\tjmp_buf trampoline;\n+\tint trampoline_enabled;\n+\n+\tcl_trace_cb *pfn_trace_cb;\n+\tvoid *trace_payload;\n+\n+} _clar;\n+\n+struct clar_func {\n+\tconst char *name;\n+\tvoid (*ptr)(void);\n+};\n+\n+struct clar_suite {\n+\tconst char *name;\n+\tstruct clar_func initialize;\n+\tstruct clar_func cleanup;\n+\tconst struct clar_func *tests;\n+\tsize_t test_count;\n+\tint enabled;\n+};\n+\n+/* From clar_print_*.c */\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n+static void clar_print_onsuite(const char *suite_name, int suite_index);\n+static void clar_print_onabort(const char *msg, ...);\n+\n+/* From clar_sandbox.c */\n+static void clar_unsandbox(void);\n+static int clar_sandbox(void);\n+\n+/* From summary.h */\n+static struct clar_summary *clar_summary_init(const char *filename);\n+static int clar_summary_shutdown(struct clar_summary *fp);\n+\n+/* Load the declarations for the test suite */\n+#include \"clar.suite\"\n+\n+\n+#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n+\t} while (0)\n+\n+void cl_trace_register(cl_trace_cb *cb, void *payload)\n+{\n+\t_clar.pfn_trace_cb = cb;\n+\t_clar.trace_payload = payload;\n+}\n+\n+\n+/* Core test functions */\n+static void\n+clar_report_errors(struct clar_report *report)\n+{\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (error = report->errors; error; error = error->next)\n+\t\tclar_print_error(i++, _clar.last_report, error);\n+}\n+\n+static void\n+clar_report_all(void)\n+{\n+\tstruct clar_report *report;\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (report = _clar.reports; report; report = report->next) {\n+\t\tif (report->status != CL_TEST_FAILURE)\n+\t\t\tcontinue;\n+\n+\t\tfor (error = report->errors; error; error = error->next)\n+\t\t\tclar_print_error(i++, report, error);\n+\t}\n+}\n+\n+#ifdef WIN32\n+# define clar_time DWORD\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\t*out = GetTickCount();\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)*end - (double)*start) / 1000;\n+}\n+#else\n+# include <sys/time.h>\n+\n+# define clar_time struct timeval\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\tstruct timezone tz;\n+\n+\tgettimeofday(out, &tz);\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n+\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n+}\n+#endif\n+\n+static void\n+clar_run_test(\n+\tconst struct clar_suite *suite,\n+\tconst struct clar_func *test,\n+\tconst struct clar_func *initialize,\n+\tconst struct clar_func *cleanup)\n+{\n+\tclar_time start, end;\n+\n+\t_clar.trampoline_enabled = 1;\n+\n+\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n+\n+\t_clar.last_report->start = time(NULL);\n+\tclar_time_now(&start);\n+\n+\tif (setjmp(_clar.trampoline) == 0) {\n+\t\tif (initialize->ptr != NULL)\n+\t\t\tinitialize->ptr();\n+\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n+\t\ttest->ptr();\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n+\t}\n+\n+\tclar_time_now(&end);\n+\n+\t_clar.trampoline_enabled = 0;\n+\n+\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n+\t\t_clar.last_report->status = CL_TEST_OK;\n+\n+\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n+\n+\tif (_clar.local_cleanup != NULL)\n+\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n+\n+\tif (cleanup->ptr != NULL)\n+\t\tcleanup->ptr();\n+\n+\tCL_TRACE(CL_TRACE__TEST__END);\n+\n+\t_clar.tests_ran++;\n+\n+\t/* remove any local-set cleanup methods */\n+\t_clar.local_cleanup = NULL;\n+\t_clar.local_cleanup_payload = NULL;\n+\n+\tif (_clar.report_errors_only) {\n+\t\tclar_report_errors(_clar.last_report);\n+\t} else {\n+\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n+\t}\n+}\n+\n+static void\n+clar_run_suite(const struct clar_suite *suite, const char *filter)\n+{\n+\tconst struct clar_func *test = suite->tests;\n+\tsize_t i, matchlen;\n+\tstruct clar_report *report;\n+\tint exact = 0;\n+\n+\tif (!suite->enabled)\n+\t\treturn;\n+\n+\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\treturn;\n+\n+\tif (!_clar.report_errors_only)\n+\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n+\n+\t_clar.active_suite = suite->name;\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n+\n+\tif (filter) {\n+\t\tsize_t suitelen = strlen(suite->name);\n+\t\tmatchlen = strlen(filter);\n+\t\tif (matchlen <= suitelen) {\n+\t\t\tfilter = NULL;\n+\t\t} else {\n+\t\t\tfilter += suitelen;\n+\t\t\twhile (*filter == ':')\n+\t\t\t\t++filter;\n+\t\t\tmatchlen = strlen(filter);\n+\n+\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n+\t\t\t\texact = 1;\n+\t\t\t\tmatchlen--;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (i = 0; i < suite->test_count; ++i) {\n+\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n+\t\t\tcontinue;\n+\n+\t\tif (exact && strlen(test[i].name) != matchlen)\n+\t\t\tcontinue;\n+\n+\t\t_clar.active_test = test[i].name;\n+\n+\t\treport = calloc(1, sizeof(struct clar_report));\n+\t\treport->suite = _clar.active_suite;\n+\t\treport->test = _clar.active_test;\n+\t\treport->test_number = _clar.tests_ran;\n+\t\treport->status = CL_TEST_NOTRUN;\n+\n+\t\tif (_clar.reports == NULL)\n+\t\t\t_clar.reports = report;\n+\n+\t\tif (_clar.last_report != NULL)\n+\t\t\t_clar.last_report->next = report;\n+\n+\t\t_clar.last_report = report;\n+\n+\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n+\n+\t\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\t\treturn;\n+\t}\n+\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_END);\n+}\n+\n+static void\n+clar_usage(const char *arg)\n+{\n+\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n+\tprintf(\"Options:\\n\");\n+\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n+\tprintf(\"  -iname        Include the suite with `name`\\n\");\n+\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n+\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n+\tprintf(\"  -q            Only report tests that had an error\\n\");\n+\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n+\tprintf(\"  -t            Display results in tap format\\n\");\n+\tprintf(\"  -l            Print suite names\\n\");\n+\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n+\texit(-1);\n+}\n+\n+static void\n+clar_parse_args(int argc, char **argv)\n+{\n+\tint i;\n+\n+\t/* Verify options before execute */\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tif (argument[0] != '-' || argument[1] == '\\0'\n+\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n+\t\t\tclar_usage(argv[0]);\n+\t\t}\n+\t}\n+\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tswitch (argument[1]) {\n+\t\tcase 's':\n+\t\tcase 'i':\n+\t\tcase 'x': { /* given suite name */\n+\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n+\t\t\tchar action = argument[1];\n+\t\t\tsize_t j, arglen, suitelen, cmplen;\n+\n+\t\t\targument += offset;\n+\t\t\targlen = strlen(argument);\n+\n+\t\t\tif (arglen == 0)\n+\t\t\t\tclar_usage(argv[0]);\n+\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n+\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n+\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n+\n+\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n+\t\t\t\t\tint exact = (arglen >= suitelen);\n+\n+\t\t\t\t\t/* Do we have a real suite prefix separated by a\n+\t\t\t\t\t * trailing '::' or just a matching substring? */\n+\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n+\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n+\t\t\t\t\t    continue;\n+\n+\t\t\t\t\t++found;\n+\n+\t\t\t\t\tif (!exact)\n+\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n+\n+\t\t\t\t\tswitch (action) {\n+\t\t\t\t\tcase 's': {\n+\t\t\t\t\t\tstruct clar_explicit *explicit =\n+\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n+\t\t\t\t\t\tassert(explicit);\n+\n+\t\t\t\t\t\texplicit->suite_idx = j;\n+\t\t\t\t\t\texplicit->filter = argument;\n+\n+\t\t\t\t\t\tif (_clar.explicit == NULL)\n+\t\t\t\t\t\t\t_clar.explicit = explicit;\n+\n+\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n+\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n+\n+\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n+\t\t\t\t\t\t_clar.last_explicit = explicit;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n+\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif (exact)\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (!found) {\n+\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n+\t\t\t\texit(-1);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase 'q':\n+\t\t\t_clar.report_errors_only = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 'Q':\n+\t\t\t_clar.exit_on_error = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 't':\n+\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n+\t\t\tbreak;\n+\n+\t\tcase 'l': {\n+\t\t\tsize_t j;\n+\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n+\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n+\n+\t\t\texit(0);\n+\t\t}\n+\n+\t\tcase 'v':\n+\t\t\t_clar.verbosity++;\n+\t\t\tbreak;\n+\n+\t\tcase 'r':\n+\t\t\t_clar.write_summary = 1;\n+\t\t\tfree(_clar.summary_filename);\n+\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tassert(!\"Unexpected commandline argument!\");\n+\t\t}\n+\t}\n+}\n+\n+void\n+clar_test_init(int argc, char **argv)\n+{\n+\tconst char *summary_env;\n+\n+\tif (argc > 1)\n+\t\tclar_parse_args(argc, argv);\n+\n+\tclar_print_init(\n+\t\t(int)_clar_callback_count,\n+\t\t(int)_clar_suite_count,\n+\t\t\"\"\n+\t);\n+\n+\tif (!_clar.summary_filename &&\n+\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n+\t\t_clar.write_summary = 1;\n+\t\t_clar.summary_filename = strdup(summary_env);\n+\t}\n+\n+\tif (_clar.write_summary && !_clar.summary_filename)\n+\t\t_clar.summary_filename = strdup(\"summary.xml\");\n+\n+\tif (_clar.write_summary &&\n+\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n+\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tif (clar_sandbox() < 0) {\n+\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n+\t\texit(-1);\n+\t}\n+}\n+\n+int\n+clar_test_run(void)\n+{\n+\tsize_t i;\n+\tstruct clar_explicit *explicit;\n+\n+\tif (_clar.explicit) {\n+\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n+\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n+\t} else {\n+\t\tfor (i = 0; i < _clar_suite_count; ++i)\n+\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n+\t}\n+\n+\treturn _clar.total_errors;\n+}\n+\n+void\n+clar_test_shutdown(void)\n+{\n+\tstruct clar_explicit *explicit, *explicit_next;\n+\tstruct clar_report *report, *report_next;\n+\n+\tclar_print_shutdown(\n+\t\t_clar.tests_ran,\n+\t\t(int)_clar_suite_count,\n+\t\t_clar.total_errors\n+\t);\n+\n+\tclar_unsandbox();\n+\n+\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n+\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n+\t\texplicit_next = explicit->next;\n+\t\tfree(explicit);\n+\t}\n+\n+\tfor (report = _clar.reports; report; report = report_next) {\n+\t\treport_next = report->next;\n+\t\tfree(report);\n+\t}\n+\n+\tfree(_clar.summary_filename);\n+}\n+\n+int\n+clar_test(int argc, char **argv)\n+{\n+\tint errors;\n+\n+\tclar_test_init(argc, argv);\n+\terrors = clar_test_run();\n+\tclar_test_shutdown();\n+\n+\treturn errors;\n+}\n+\n+static void abort_test(void)\n+{\n+\tif (!_clar.trampoline_enabled) {\n+\t\tclar_print_onabort(\n+\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n+\t\tclar_report_errors(_clar.last_report);\n+\t\texit(-1);\n+\t}\n+\n+\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n+\tlongjmp(_clar.trampoline, -1);\n+}\n+\n+void clar__skip(void)\n+{\n+\t_clar.last_report->status = CL_TEST_SKIP;\n+\t_clar.total_skipped++;\n+\tabort_test();\n+}\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n+\n+\tif (_clar.last_report->errors == NULL)\n+\t\t_clar.last_report->errors = error;\n+\n+\tif (_clar.last_report->last_error != NULL)\n+\t\t_clar.last_report->last_error->next = error;\n+\n+\t_clar.last_report->last_error = error;\n+\n+\terror->file = file;\n+\terror->function = function;\n+\terror->line_number = line;\n+\terror->error_msg = error_msg;\n+\n+\tif (description != NULL)\n+\t\terror->description = strdup(description);\n+\n+\t_clar.total_errors++;\n+\t_clar.last_report->status = CL_TEST_FAILURE;\n+\n+\tif (should_abort)\n+\t\tabort_test();\n+}\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tif (condition)\n+\t\treturn;\n+\n+\tclar__fail(file, function, line, error_msg, description, should_abort);\n+}\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...)\n+{\n+\tva_list args;\n+\tchar buf[4096];\n+\tint is_equal = 1;\n+\n+\tva_start(args, fmt);\n+\n+\tif (!strcmp(\"%s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n+\t\t\t\t\ts1, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n+\t\t\t\t\tlen, s1, len, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n+\t\t\t\t\twcs1, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n+\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n+\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n+\t\tis_equal = (sz1 == sz2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%p\", fmt)) {\n+\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n+\t\tis_equal = (p1 == p2);\n+\t\tif (!is_equal)\n+\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n+\t}\n+\telse {\n+\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n+\t\tis_equal = (i1 == i2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n+\t\t}\n+\t}\n+\n+\tva_end(args);\n+\n+\tif (!is_equal)\n+\t\tclar__fail(file, function, line, err, buf, should_abort);\n+}\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n+{\n+\t_clar.local_cleanup = cleanup;\n+\t_clar.local_cleanup_payload = opaque;\n+}\n+\n+#include \"clar/sandbox.h\"\n+#include \"clar/fixtures.h\"\n+#include \"clar/fs.h\"\n+#include \"clar/print.h\"\n+#include \"clar/summary.h\"\ndiff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\nnew file mode 100644\nindex 00000000000..8c22382bd56\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.h\n@@ -0,0 +1,173 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST_H__\n+#define __CLAR_TEST_H__\n+\n+#include <stdlib.h>\n+\n+enum cl_test_status {\n+\tCL_TEST_OK,\n+\tCL_TEST_FAILURE,\n+\tCL_TEST_SKIP,\n+\tCL_TEST_NOTRUN,\n+};\n+\n+enum cl_output_format {\n+\tCL_OUTPUT_CLAP,\n+\tCL_OUTPUT_TAP,\n+};\n+\n+/** Setup clar environment */\n+void clar_test_init(int argc, char *argv[]);\n+int clar_test_run(void);\n+void clar_test_shutdown(void);\n+\n+/** One shot setup & run */\n+int clar_test(int argc, char *argv[]);\n+\n+const char *clar_sandbox_path(void);\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n+void cl_fs_cleanup(void);\n+\n+/**\n+ * cl_trace_* is a hook to provide a simple global tracing\n+ * mechanism.\n+ *\n+ * The goal here is to let main() provide clar-proper\n+ * with a callback to optionally write log info for\n+ * test operations into the same stream used by their\n+ * actual tests.  This would let them print test names\n+ * and maybe performance data as they choose.\n+ *\n+ * The goal is NOT to alter the flow of control or to\n+ * override test selection/skipping.  (So the callback\n+ * does not return a value.)\n+ *\n+ * The goal is NOT to duplicate the existing\n+ * pass/fail/skip reporting.  (So the callback\n+ * does not accept a status/errorcode argument.)\n+ *\n+ */\n+typedef enum cl_trace_event {\n+\tCL_TRACE__SUITE_BEGIN,\n+\tCL_TRACE__SUITE_END,\n+\tCL_TRACE__TEST__BEGIN,\n+\tCL_TRACE__TEST__END,\n+\tCL_TRACE__TEST__RUN_BEGIN,\n+\tCL_TRACE__TEST__RUN_END,\n+\tCL_TRACE__TEST__LONGJMP,\n+} cl_trace_event;\n+\n+typedef void (cl_trace_cb)(\n+\tcl_trace_event ev,\n+\tconst char *suite_name,\n+\tconst char *test_name,\n+\tvoid *payload);\n+\n+/**\n+ * Register a callback into CLAR to send global trace events.\n+ * Pass NULL to disable.\n+ */\n+void cl_trace_register(cl_trace_cb *cb, void *payload);\n+\n+\n+#ifdef CLAR_FIXTURE_PATH\n+const char *cl_fixture(const char *fixture_name);\n+void cl_fixture_sandbox(const char *fixture_name);\n+void cl_fixture_cleanup(const char *fixture_name);\n+const char *cl_fixture_basename(const char *fixture_name);\n+#endif\n+\n+/**\n+ * Assertion macros with explicit error message\n+ */\n+#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n+#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n+#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n+\n+/**\n+ * Check macros with explicit error message\n+ */\n+#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n+#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n+#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n+\n+/**\n+ * Assertion macros with no error message\n+ */\n+#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n+#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n+#define cl_assert(expr) cl_assert_(expr, NULL)\n+\n+/**\n+ * Check macros with no error message\n+ */\n+#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n+#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n+#define cl_check(expr) cl_check_(expr, NULL)\n+\n+/**\n+ * Forced failure/warning\n+ */\n+#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n+#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n+\n+#define cl_skip() clar__skip()\n+\n+/**\n+ * Typed assertion macros\n+ */\n+#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n+#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n+\n+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n+\n+#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n+#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n+\n+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+\n+#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n+#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n+#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n+\n+#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n+\n+#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n+\n+void clar__skip(void);\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...);\n+\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\nnew file mode 100644\nindex 00000000000..6ec6423484d\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fixtures.h\n@@ -0,0 +1,50 @@\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name)\n+{\n+\tstatic char _path[4096];\n+\tsize_t root_len;\n+\n+\troot_len = strlen(base);\n+\tstrncpy(_path, base, sizeof(_path));\n+\n+\tif (_path[root_len - 1] != '/')\n+\t\t_path[root_len++] = '/';\n+\n+\tif (fixture_name[0] == '/')\n+\t\tfixture_name++;\n+\n+\tstrncpy(_path + root_len,\n+\t\tfixture_name,\n+\t\tsizeof(_path) - root_len);\n+\n+\treturn _path;\n+}\n+\n+const char *cl_fixture(const char *fixture_name)\n+{\n+\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n+}\n+\n+void cl_fixture_sandbox(const char *fixture_name)\n+{\n+\tfs_copy(cl_fixture(fixture_name), _clar_path);\n+}\n+\n+const char *cl_fixture_basename(const char *fixture_name)\n+{\n+\tconst char *p;\n+\n+\tfor (p = fixture_name; *p; p++) {\n+\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n+\t\t\tfixture_name = p+1;\n+\t}\n+\n+\treturn fixture_name;\n+}\n+\n+void cl_fixture_cleanup(const char *fixture_name)\n+{\n+\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nnew file mode 100644\nindex 00000000000..3e39890bd3e\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -0,0 +1,522 @@\n+/*\n+ * By default, use a read/write loop to copy files on POSIX systems.\n+ * On Linux, use sendfile by default as it's slightly faster.  On\n+ * macOS, we avoid fcopyfile by default because it's slightly slower.\n+ */\n+#undef USE_FCOPYFILE\n+#define USE_SENDFILE 1\n+\n+#ifdef _WIN32\n+\n+#ifdef CLAR_WIN32_LONGPATHS\n+# define CLAR_MAX_PATH 4096\n+#else\n+# define CLAR_MAX_PATH MAX_PATH\n+#endif\n+\n+#define RM_RETRY_COUNT\t5\n+#define RM_RETRY_DELAY\t10\n+\n+#ifdef __MINGW32__\n+\n+/* These security-enhanced functions are not available\n+ * in MinGW, so just use the vanilla ones */\n+#define wcscpy_s(a, b, c) wcscpy((a), (c))\n+#define wcscat_s(a, b, c) wcscat((a), (c))\n+\n+#endif /* __MINGW32__ */\n+\n+static int\n+fs__dotordotdot(WCHAR *_tocheck)\n+{\n+\treturn _tocheck[0] == '.' &&\n+\t\t(_tocheck[1] == '\\0' ||\n+\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n+}\n+\n+static int\n+fs_rmdir_rmdir(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\n+\twhile (!RemoveDirectoryW(_wpath)) {\n+\t\t/* Only retry when we have retries remaining, and the\n+\t\t * error was ERROR_DIR_NOT_EMPTY. */\n+\t\tif (retries++ > RM_RETRY_COUNT ||\n+\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n+\t\t\treturn -1;\n+\n+\t\t/* Give whatever has a handle to a child item some time\n+\t\t * to release it before trying again */\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void translate_path(WCHAR *path, size_t path_size)\n+{\n+    size_t path_len, i;\n+\n+    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n+\treturn;\n+\n+    path_len = wcslen(path);\n+    cl_assert(path_size > path_len + 4);\n+\n+    for (i = path_len; i > 0; i--) {\n+\tWCHAR c = path[i - 1];\n+\n+\tif (c == L'/')\n+\t    path[i + 3] = L'\\\\';\n+\telse\n+\t    path[i + 3] = path[i - 1];\n+    }\n+\n+    path[0] = L'\\\\';\n+    path[1] = L'\\\\';\n+    path[2] = L'?';\n+    path[3] = L'\\\\';\n+    path[path_len + 4] = L'\\0';\n+}\n+\n+static void\n+fs_rmdir_helper(WCHAR *_wsource)\n+{\n+\tWCHAR buffer[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buffer_prefix_len;\n+\n+\t/* Set up the buffer and capture the length */\n+\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n+\ttranslate_path(buffer, CLAR_MAX_PATH);\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n+\tbuffer_prefix_len = wcslen(buffer);\n+\n+\t/* FindFirstFile needs a wildcard to match multiple items */\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buffer, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_rmdir_helper(buffer);\n+\t\telse {\n+\t\t\t/* If set, the +R bit must be cleared before deleting */\n+\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n+\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\t\tcl_assert(DeleteFileW(buffer));\n+\t\t}\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+\n+\t/* Now that the directory is empty, remove it */\n+\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n+}\n+\n+static int\n+fs_rm_wait(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\tDWORD last_error;\n+\n+\tdo {\n+\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n+\t\t\tlast_error = GetLastError();\n+\t\telse\n+\t\t\tlast_error = ERROR_SUCCESS;\n+\n+\t\t/* Is the item gone? */\n+\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n+\t\t\tERROR_PATH_NOT_FOUND == last_error)\n+\t\t\treturn 0;\n+\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\twhile (retries++ <= RM_RETRY_COUNT);\n+\n+\treturn -1;\n+}\n+\n+static void\n+fs_rm(const char *_source)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH];\n+\tDWORD attrs;\n+\n+\t/* The input path is UTF-8. Convert it to wide characters\n+\t * for use with the Windows API */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1, /* Indicates NULL termination */\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\n+\t/* Does the item exist? If not, we have no work to do */\n+\tattrs = GetFileAttributesW(wsource);\n+\n+\tif (INVALID_FILE_ATTRIBUTES == attrs)\n+\t\treturn;\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n+\t\tfs_rmdir_helper(wsource);\n+\telse {\n+\t\t/* The item is a file. Strip the +R bit */\n+\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n+\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\tcl_assert(DeleteFileW(wsource));\n+\t}\n+\n+\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n+\tcl_assert(0 == fs_rm_wait(wsource));\n+}\n+\n+static void\n+fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n+{\n+\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n+\n+\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_source, CLAR_MAX_PATH);\n+\tbuf_source_prefix_len = wcslen(buf_source);\n+\n+\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n+\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n+\tbuf_dest_prefix_len = wcslen(buf_dest);\n+\n+\t/* Get an enumerator for the items in the source. */\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buf_source, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\t/* Create the target directory. */\n+\tcl_assert(CreateDirectoryW(_wdest, NULL));\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n+\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_copydir_helper(buf_source, buf_dest);\n+\t\telse\n+\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+}\n+\n+static void\n+fs_copy(const char *_source, const char *_dest)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n+\tDWORD source_attrs, dest_attrs;\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\n+\t/* The input paths are UTF-8. Convert them to wide characters\n+\t * for use with the Windows API. */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1,\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_dest,\n+\t\t\t\t-1,\n+\t\t\t\twdest,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\ttranslate_path(wdest, CLAR_MAX_PATH);\n+\n+\t/* Check the source for existence */\n+\tsource_attrs = GetFileAttributesW(wsource);\n+\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n+\n+\t/* Check the target for existence */\n+\tdest_attrs = GetFileAttributesW(wdest);\n+\n+\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n+\t\t/* Target exists; append last path part of source to target.\n+\t\t * Use FindFirstFile to parse the path */\n+\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n+\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n+\t\tFindClose(find_handle);\n+\n+\t\t/* Check the new target for existence */\n+\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n+\t}\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n+\t\tfs_copydir_helper(wsource, wdest);\n+\telse\n+\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+#ifdef CLAR_FIXTURE_PATH\n+\tfs_rm(fixture_path(_clar_path, \"*\"));\n+#endif\n+}\n+\n+#else\n+\n+#include <errno.h>\n+#include <string.h>\n+#include <limits.h>\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#if defined(__linux__)\n+# include <sys/sendfile.h>\n+#endif\n+\n+#if defined(__APPLE__)\n+# include <copyfile.h>\n+#endif\n+\n+static void basename_r(const char **out, int *out_len, const char *in)\n+{\n+\tsize_t in_len = strlen(in), start_pos;\n+\n+\tfor (in_len = strlen(in); in_len; in_len--) {\n+\t\tif (in[in_len - 1] != '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tfor (start_pos = in_len; start_pos; start_pos--) {\n+\t\tif (in[start_pos - 1] == '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tcl_assert(in_len - start_pos < INT_MAX);\n+\n+\tif (in_len - start_pos > 0) {\n+\t\t*out = &in[start_pos];\n+\t\t*out_len = (in_len - start_pos);\n+\t} else {\n+\t\t*out = \"/\";\n+\t\t*out_len = 1;\n+\t}\n+}\n+\n+static char *joinpath(const char *dir, const char *base, int base_len)\n+{\n+\tchar *out;\n+\tint len;\n+\n+\tif (base_len == -1) {\n+\t\tsize_t bl = strlen(base);\n+\n+\t\tcl_assert(bl < INT_MAX);\n+\t\tbase_len = (int)bl;\n+\t}\n+\n+\tlen = strlen(dir) + base_len + 2;\n+\tcl_assert(len > 0);\n+\n+\tcl_assert(out = malloc(len));\n+\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n+\n+\treturn out;\n+}\n+\n+static void\n+fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n+{\n+\tDIR *source_dir;\n+\tstruct dirent *d;\n+\n+\tmkdir(dest, dest_mode);\n+\n+\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n+\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(source, d->d_name, -1);\n+\t\tfs_copy(child, dest);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\n+\tclosedir(source_dir);\n+}\n+\n+static void\n+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n+{\n+\tint in, out;\n+\n+\tcl_must_pass((in = open(source, O_RDONLY)));\n+\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n+\n+#if USE_FCOPYFILE && defined(__APPLE__)\n+\t((void)(source_len)); /* unused */\n+\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n+#elif USE_SENDFILE && defined(__linux__)\n+\t{\n+\t\tssize_t ret = 0;\n+\n+\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n+\t\t\tsource_len -= (size_t)ret;\n+\t\t}\n+\t\tcl_assert(ret >= 0);\n+\t}\n+#else\n+\t{\n+\t\tchar buf[131072];\n+\t\tssize_t ret;\n+\n+\t\t((void)(source_len)); /* unused */\n+\n+\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n+\t\t\tsize_t len = (size_t)ret;\n+\n+\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n+\t\t\t\tcl_assert(ret <= (ssize_t)len);\n+\t\t\t\tlen -= ret;\n+\t\t\t}\n+\t\t\tcl_assert(ret >= 0);\n+\t\t}\n+\t\tcl_assert(ret == 0);\n+\t}\n+#endif\n+\n+\tclose(in);\n+\tclose(out);\n+}\n+\n+static void\n+fs_copy(const char *source, const char *_dest)\n+{\n+\tchar *dbuf = NULL;\n+\tconst char *dest = NULL;\n+\tstruct stat source_st, dest_st;\n+\n+\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n+\n+\tif (lstat(_dest, &dest_st) == 0) {\n+\t\tconst char *base;\n+\t\tint base_len;\n+\n+\t\t/* Target exists and is directory; append basename */\n+\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n+\n+\t\tbasename_r(&base, &base_len, source);\n+\t\tcl_assert(base_len < INT_MAX);\n+\n+\t\tdbuf = joinpath(_dest, base, base_len);\n+\t\tdest = dbuf;\n+\t} else if (errno != ENOENT) {\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t} else {\n+\t\tdest = _dest;\n+\t}\n+\n+\tif (S_ISDIR(source_st.st_mode)) {\n+\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n+\t} else {\n+\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n+\t}\n+\n+\tfree(dbuf);\n+}\n+\n+static void\n+fs_rmdir_helper(const char *path)\n+{\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n+\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(path, d->d_name, -1);\n+\t\tfs_rm(child);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\tclosedir(dir);\n+\n+\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n+}\n+\n+static void\n+fs_rm(const char *path)\n+{\n+\tstruct stat st;\n+\n+\tif (lstat(path, &st)) {\n+\t\tif (errno == ENOENT)\n+\t\t\treturn;\n+\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t}\n+\n+\tif (S_ISDIR(st.st_mode)) {\n+\t\tfs_rmdir_helper(path);\n+\t} else {\n+\t\tcl_must_pass(unlink(path));\n+\t}\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+\tclar_unsandbox();\n+\tclar_sandbox();\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\nnew file mode 100644\nindex 00000000000..c17e2f693bd\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/print.h\n@@ -0,0 +1,211 @@\n+/* clap: clar protocol, the traditional clar output format */\n+\n+static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n+\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n+}\n+\n+static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"\\n\\n\");\n+\tclar_report_all();\n+}\n+\n+static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tprintf(\"  %d) Failure:\\n\", num);\n+\n+\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n+\t\treport->suite,\n+\t\treport->test,\n+\t\terror->file,\n+\t\terror->line_number);\n+\n+\tprintf(\"  %s\\n\", error->error_msg);\n+\n+\tif (error->description != NULL)\n+\t\tprintf(\"  %s\\n\", error->description);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tif (_clar.verbosity > 1) {\n+\t\tprintf(\"%s::%s: \", suite_name, test_name);\n+\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n+\t\t}\n+\t} else {\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\".\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n+\t\t}\n+\n+\t\tfflush(stdout);\n+\t}\n+}\n+\n+static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tif (_clar.verbosity == 1)\n+\t\tprintf(\"\\n%s\", suite_name);\n+\n+\t(void)suite_index;\n+}\n+\n+static void clar_print_clap_onabort(const char *fmt, va_list arg)\n+{\n+\tvfprintf(stderr, fmt, arg);\n+}\n+\n+/* tap: test anywhere protocol format */\n+\n+static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)suite_names;\n+\tprintf(\"TAP version 13\\n\");\n+}\n+\n+static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"1..%d\\n\", test_count);\n+}\n+\n+static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\t(void)num;\n+\t(void)report;\n+\t(void)error;\n+}\n+\n+static void print_escaped(const char *str)\n+{\n+\tchar *c;\n+\n+\twhile ((c = strchr(str, '\\'')) != NULL) {\n+\t\tprintf(\"%.*s\", (int)(c - str), str);\n+\t\tprintf(\"''\");\n+\t\tstr = c + 1;\n+\t}\n+\n+\tprintf(\"%s\", str);\n+}\n+\n+static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tconst struct clar_error *error = _clar.last_report->errors;\n+\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tswitch(status) {\n+\tcase CL_TEST_OK:\n+\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\tcase CL_TEST_FAILURE:\n+\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\n+\t\tprintf(\"    ---\\n\");\n+\t\tprintf(\"    reason: |\\n\");\n+\t\tprintf(\"      %s\\n\", error->error_msg);\n+\n+\t\tif (error->description)\n+\t\t\tprintf(\"      %s\\n\", error->description);\n+\n+\t\tprintf(\"    at:\\n\");\n+\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n+\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n+\t\tprintf(\"      function: '%s'\\n\", error->function);\n+\t\tprintf(\"    ---\\n\");\n+\n+\t\tbreak;\n+\tcase CL_TEST_SKIP:\n+\tcase CL_TEST_NOTRUN:\n+\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\t}\n+\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n+}\n+\n+static void clar_print_tap_onabort(const char *fmt, va_list arg)\n+{\n+\tprintf(\"Bail out! \");\n+\tvprintf(fmt, arg);\n+\tfflush(stdout);\n+}\n+\n+/* indirection between protocol output selection */\n+\n+#define PRINT(FN, ...) do { \\\n+\t\tswitch (_clar.output_format) { \\\n+\t\t\tcase CL_OUTPUT_CLAP: \\\n+\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tcase CL_OUTPUT_TAP: \\\n+\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tdefault: \\\n+\t\t\t\tabort(); \\\n+\t\t} \\\n+\t} while (0)\n+\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\tPRINT(init, test_count, suite_count, suite_names);\n+}\n+\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\tPRINT(shutdown, test_count, suite_count, error_count);\n+}\n+\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tPRINT(error, num, report, error);\n+}\n+\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tPRINT(ontest, suite_name, test_name, test_number, status);\n+}\n+\n+static void clar_print_onsuite(const char *suite_name, int suite_index)\n+{\n+\tPRINT(onsuite, suite_name, suite_index);\n+}\n+\n+static void clar_print_onabort(const char *msg, ...)\n+{\n+\tva_list argp;\n+\tva_start(argp, msg);\n+\tPRINT(onabort, msg, argp);\n+\tva_end(argp);\n+}\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nnew file mode 100644\nindex 00000000000..7c177f35258\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -0,0 +1,153 @@\n+#ifdef __APPLE__\n+#include <sys/syslimits.h>\n+#endif\n+\n+static char _clar_path[4096 + 1];\n+\n+static int\n+is_valid_tmp_path(const char *path)\n+{\n+\tSTAT_T st;\n+\n+\tif (stat(path, &st) != 0)\n+\t\treturn 0;\n+\n+\tif (!S_ISDIR(st.st_mode))\n+\t\treturn 0;\n+\n+\treturn (access(path, W_OK) == 0);\n+}\n+\n+static int\n+find_tmp_path(char *buffer, size_t length)\n+{\n+#ifndef _WIN32\n+\tstatic const size_t var_count = 5;\n+\tstatic const char *env_vars[] = {\n+\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n+\t};\n+\n+\tsize_t i;\n+\n+\tfor (i = 0; i < var_count; ++i) {\n+\t\tconst char *env = getenv(env_vars[i]);\n+\t\tif (!env)\n+\t\t\tcontinue;\n+\n+\t\tif (is_valid_tmp_path(env)) {\n+#ifdef __APPLE__\n+\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n+\t\t\t\treturn 0;\n+#endif\n+\t\t\tstrncpy(buffer, env, length - 1);\n+\t\t\tbuffer[length - 1] = '\\0';\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\t/* If the environment doesn't say anything, try to use /tmp */\n+\tif (is_valid_tmp_path(\"/tmp\")) {\n+#ifdef __APPLE__\n+\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n+\t\t\treturn 0;\n+#endif\n+\t\tstrncpy(buffer, \"/tmp\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+#else\n+\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n+\tif (env_len > 0 && env_len < (DWORD)length)\n+\t\treturn 0;\n+\n+\tif (GetTempPath((DWORD)length, buffer))\n+\t\treturn 0;\n+#endif\n+\n+\t/* This system doesn't like us, try to use the current directory */\n+\tif (is_valid_tmp_path(\".\")) {\n+\t\tstrncpy(buffer, \".\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static void clar_unsandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0')\n+\t\treturn;\n+\n+\tcl_must_pass(chdir(\"..\"));\n+\n+\tfs_rm(_clar_path);\n+}\n+\n+static int build_sandbox_path(void)\n+{\n+#ifdef CLAR_TMPDIR\n+\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n+#else\n+\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n+#endif\n+\n+\tsize_t len;\n+\n+\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n+\t\treturn -1;\n+\n+\tlen = strlen(_clar_path);\n+\n+#ifdef _WIN32\n+\t{ /* normalize path to POSIX forward slashes */\n+\t\tsize_t i;\n+\t\tfor (i = 0; i < len; ++i) {\n+\t\t\tif (_clar_path[i] == '\\\\')\n+\t\t\t\t_clar_path[i] = '/';\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (_clar_path[len - 1] != '/') {\n+\t\t_clar_path[len++] = '/';\n+\t}\n+\n+\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n+\n+#if defined(__MINGW32__)\n+\tif (_mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(_WIN32)\n+\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#else\n+\tif (mkdtemp(_clar_path) == NULL)\n+\t\treturn -1;\n+#endif\n+\n+\treturn 0;\n+}\n+\n+static int clar_sandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n+\t\treturn -1;\n+\n+\tif (chdir(_clar_path) != 0)\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const char *clar_sandbox_path(void)\n+{\n+\treturn _clar_path;\n+}\ndiff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\nnew file mode 100644\nindex 00000000000..4dd352e28b8\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/summary.h\n@@ -0,0 +1,143 @@\n+\n+#include <stdio.h>\n+#include <time.h>\n+\n+static int clar_summary_close_tag(\n+    struct clar_summary *summary, const char *tag, int indent)\n+{\n+\tconst char *indt;\n+\n+\tif (indent == 0) indt = \"\";\n+\telse if (indent == 1) indt = \"\\t\";\n+\telse indt = \"\\t\\t\";\n+\n+\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n+}\n+\n+static int clar_summary_testsuites(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n+}\n+\n+static int clar_summary_testsuite(struct clar_summary *summary,\n+    int idn, const char *name, time_t timestamp,\n+    int test_count, int fail_count, int error_count)\n+{\n+\tstruct tm *tm = localtime(&timestamp);\n+\tchar iso_dt[20];\n+\n+\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n+\t\treturn -1;\n+\n+\treturn fprintf(summary->fp, \"\\t<testsuite\"\n+\t\t       \" id=\\\"%d\\\"\"\n+\t\t       \" name=\\\"%s\\\"\"\n+\t\t       \" hostname=\\\"localhost\\\"\"\n+\t\t       \" timestamp=\\\"%s\\\"\"\n+\t\t       \" tests=\\\"%d\\\"\"\n+\t\t       \" failures=\\\"%d\\\"\"\n+\t\t       \" errors=\\\"%d\\\">\\n\",\n+\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n+}\n+\n+static int clar_summary_testcase(struct clar_summary *summary,\n+    const char *name, const char *classname, double elapsed)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n+\t\tname, classname, elapsed);\n+}\n+\n+static int clar_summary_failure(struct clar_summary *summary,\n+    const char *type, const char *message, const char *desc)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n+\t    type, message, desc);\n+}\n+\n+static int clar_summary_skipped(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n+}\n+\n+struct clar_summary *clar_summary_init(const char *filename)\n+{\n+\tstruct clar_summary *summary;\n+\tFILE *fp;\n+\n+\tif ((fp = fopen(filename, \"w\")) == NULL) {\n+\t\tperror(\"fopen\");\n+\t\treturn NULL;\n+\t}\n+\n+\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n+\t\tperror(\"malloc\");\n+\t\tfclose(fp);\n+\t\treturn NULL;\n+\t}\n+\n+\tsummary->filename = filename;\n+\tsummary->fp = fp;\n+\n+\treturn summary;\n+}\n+\n+int clar_summary_shutdown(struct clar_summary *summary)\n+{\n+\tstruct clar_report *report;\n+\tconst char *last_suite = NULL;\n+\n+\tif (clar_summary_testsuites(summary) < 0)\n+\t\tgoto on_error;\n+\n+\treport = _clar.reports;\n+\twhile (report != NULL) {\n+\t\tstruct clar_error *error = report->errors;\n+\n+\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n+\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\n+\t\tlast_suite = report->suite;\n+\n+\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n+\n+\t\twhile (error != NULL) {\n+\t\t\tif (clar_summary_failure(summary, \"assert\",\n+\t\t\t    error->error_msg, error->description) < 0)\n+\t\t\t\tgoto on_error;\n+\n+\t\t\terror = error->next;\n+\t\t}\n+\n+\t\tif (report->status == CL_TEST_SKIP)\n+\t\t\tclar_summary_skipped(summary);\n+\n+\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n+\t\t\tgoto on_error;\n+\n+\t\treport = report->next;\n+\n+\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\t}\n+\n+\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n+\t    fclose(summary->fp) != 0)\n+\t\tgoto on_error;\n+\n+\tprintf(\"written summary file to %s\\n\", summary->filename);\n+\n+\tfree(summary);\n+\treturn 0;\n+\n+on_error:\n+\tfclose(summary->fp);\n+\tfree(summary);\n+\treturn -1;\n+}\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nnew file mode 100755\nindex 00000000000..80996ac3e71\n--- /dev/null\n+++ b/t/unit-tests/clar/generate.py\n@@ -0,0 +1,266 @@\n+#!/usr/bin/env python\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+from __future__ import with_statement\n+from string import Template\n+import re, fnmatch, os, sys, codecs, pickle\n+\n+class Module(object):\n+    class Template(object):\n+        def __init__(self, module):\n+            self.module = module\n+\n+        def _render_callback(self, cb):\n+            if not cb:\n+                return '    { NULL, NULL }'\n+            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n+\n+    class DeclarationTemplate(Template):\n+        def render(self):\n+            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n+\n+            for initializer in self.module.initializers:\n+                out += \"extern %s;\\n\" % initializer['declaration']\n+\n+            if self.module.cleanup:\n+                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n+\n+            return out\n+\n+    class CallbacksTemplate(Template):\n+        def render(self):\n+            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n+            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n+            out += \"\\n};\\n\"\n+            return out\n+\n+    class InfoTemplate(Template):\n+        def render(self):\n+            templates = []\n+\n+            initializers = self.module.initializers\n+            if len(initializers) == 0:\n+                initializers = [ None ]\n+\n+            for initializer in initializers:\n+                name = self.module.clean_name()\n+                if initializer and initializer['short_name'].startswith('initialize_'):\n+                    variant = initializer['short_name'][len('initialize_'):]\n+                    name += \" (%s)\" % variant.replace('_', ' ')\n+\n+                template = Template(\n+            r\"\"\"\n+    {\n+        \"${clean_name}\",\n+    ${initialize},\n+    ${cleanup},\n+        ${cb_ptr}, ${cb_count}, ${enabled}\n+    }\"\"\"\n+                ).substitute(\n+                    clean_name = name,\n+                    initialize = self._render_callback(initializer),\n+                    cleanup = self._render_callback(self.module.cleanup),\n+                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n+                    cb_count = len(self.module.callbacks),\n+                    enabled = int(self.module.enabled)\n+                )\n+                templates.append(template)\n+\n+            return ','.join(templates)\n+\n+    def __init__(self, name):\n+        self.name = name\n+\n+        self.mtime = None\n+        self.enabled = True\n+        self.modified = False\n+\n+    def clean_name(self):\n+        return self.name.replace(\"_\", \"::\")\n+\n+    def _skip_comments(self, text):\n+        SKIP_COMMENTS_REGEX = re.compile(\n+            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n+            re.DOTALL | re.MULTILINE)\n+\n+        def _replacer(match):\n+            s = match.group(0)\n+            return \"\" if s.startswith('/') else s\n+\n+        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n+\n+    def parse(self, contents):\n+        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n+\n+        contents = self._skip_comments(contents)\n+        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n+\n+        self.callbacks = []\n+        self.initializers = []\n+        self.cleanup = None\n+\n+        for (declaration, symbol, short_name) in regex.findall(contents):\n+            data = {\n+                \"short_name\" : short_name,\n+                \"declaration\" : declaration,\n+                \"symbol\" : symbol\n+            }\n+\n+            if short_name.startswith('initialize'):\n+                self.initializers.append(data)\n+            elif short_name == 'cleanup':\n+                self.cleanup = data\n+            else:\n+                self.callbacks.append(data)\n+\n+        return self.callbacks != []\n+\n+    def refresh(self, path):\n+        self.modified = False\n+\n+        try:\n+            st = os.stat(path)\n+\n+            # Not modified\n+            if st.st_mtime == self.mtime:\n+                return True\n+\n+            self.modified = True\n+            self.mtime = st.st_mtime\n+\n+            with codecs.open(path, encoding='utf-8') as fp:\n+                raw_content = fp.read()\n+\n+        except IOError:\n+            return False\n+\n+        return self.parse(raw_content)\n+\n+class TestSuite(object):\n+\n+    def __init__(self, path, output):\n+        self.path = path\n+        self.output = output\n+\n+    def should_generate(self, path):\n+        if not os.path.isfile(path):\n+            return True\n+\n+        if any(module.modified for module in self.modules.values()):\n+            return True\n+\n+        return False\n+\n+    def find_modules(self):\n+        modules = []\n+        for root, _, files in os.walk(self.path):\n+            module_root = root[len(self.path):]\n+            module_root = [c for c in module_root.split(os.sep) if c]\n+\n+            tests_in_module = fnmatch.filter(files, \"*.c\")\n+\n+            for test_file in tests_in_module:\n+                full_path = os.path.join(root, test_file)\n+                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n+\n+                modules.append((full_path, module_name))\n+\n+        return modules\n+\n+    def load_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        cache = {}\n+\n+        try:\n+            fp = open(path, 'rb')\n+            cache = pickle.load(fp)\n+            fp.close()\n+        except (IOError, ValueError):\n+            pass\n+\n+        return cache\n+\n+    def save_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        with open(path, 'wb') as cache:\n+            pickle.dump(self.modules, cache)\n+\n+    def load(self, force = False):\n+        module_data = self.find_modules()\n+        self.modules = {} if force else self.load_cache()\n+\n+        for path, name in module_data:\n+            if name not in self.modules:\n+                self.modules[name] = Module(name)\n+\n+            if not self.modules[name].refresh(path):\n+                del self.modules[name]\n+\n+    def disable(self, excluded):\n+        for exclude in excluded:\n+            for module in self.modules.values():\n+                name = module.clean_name()\n+                if name.startswith(exclude):\n+                    module.enabled = False\n+                    module.modified = True\n+\n+    def suite_count(self):\n+        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n+\n+    def callback_count(self):\n+        return sum(len(module.callbacks) for module in self.modules.values())\n+\n+    def write(self):\n+        output = os.path.join(self.output, 'clar.suite')\n+\n+        if not self.should_generate(output):\n+            return False\n+\n+        with open(output, 'w') as data:\n+            modules = sorted(self.modules.values(), key=lambda module: module.name)\n+\n+            for module in modules:\n+                t = Module.DeclarationTemplate(module)\n+                data.write(t.render())\n+\n+            for module in modules:\n+                t = Module.CallbacksTemplate(module)\n+                data.write(t.render())\n+\n+            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n+                Module.InfoTemplate(module).render() for module in modules\n+            ) + \"\\n};\\n\"\n+\n+            data.write(suites)\n+\n+            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n+            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n+\n+        self.save_cache()\n+        return True\n+\n+if __name__ == '__main__':\n+    from optparse import OptionParser\n+\n+    parser = OptionParser()\n+    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n+    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n+    parser.add_option('-o', '--output', dest='output')\n+\n+    options, args = parser.parse_args()\n+    if len(args) > 1:\n+        print(\"More than one path given\")\n+        sys.exit(1)\n+\n+    path = args.pop() if args else '.'\n+    output = options.output or path\n+    suite = TestSuite(path, output)\n+    suite.load(options.force)\n+    suite.disable(options.excluded)\n+    if suite.write():\n+        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nnew file mode 100644\nindex 00000000000..a477d0c40ca\n--- /dev/null\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -0,0 +1,4 @@\n+clar.suite\n+.clarcache\n+clar_test\n+*.o\ndiff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\nnew file mode 100644\nindex 00000000000..93c6b2ad32c\n--- /dev/null\n+++ b/t/unit-tests/clar/test/Makefile\n@@ -0,0 +1,39 @@\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+#\n+# Set up the path to the clar sources and to the fixtures directory\n+#\n+# The fixture path needs to be an absolute path so it can be used\n+# even after we have chdir'ed into the test directory while testing.\n+#\n+CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n+TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n+CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n+CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n+\n+CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n+\n+.PHONY: clean\n+\n+# list the objects that go into our test\n+objects = main.o sample.o\n+\n+# build the test executable itself\n+clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n+\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n+\n+# test object files depend on clar macros\n+$(objects) : $(CLAR_PATH)clar.h\n+\n+# build the clar.suite file of test metadata\n+clar.suite:\n+\tpython \"$(CLAR_PATH)generate.py\" .\n+\n+# remove all generated files\n+clean:\n+\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\ndiff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\nnew file mode 100644\nindex 00000000000..0fcaa639aa8\n--- /dev/null\n+++ b/t/unit-tests/clar/test/clar_test.h\n@@ -0,0 +1,16 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST__\n+#define __CLAR_TEST__\n+\n+/* Import the standard clar helper functions */\n+#include \"clar.h\"\n+\n+/* Your custom shared includes / defines here */\n+extern int global_test_counter;\n+\n+#endif\ndiff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\nnew file mode 100644\nindex 00000000000..59e56ad255b\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c\n@@ -0,0 +1,40 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Sample main() for clar tests.\n+ *\n+ * You should write your own main routine for clar tests that does specific\n+ * setup and teardown as necessary for your application.  The only required\n+ * line is the call to `clar_test(argc, argv)`, which will execute the test\n+ * suite.  If you want to check the return value of the test application,\n+ * your main() should return the same value returned by clar_test().\n+ */\n+\n+int global_test_counter = 0;\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\tint ret;\n+\n+\t/* Your custom initialization here */\n+\tglobal_test_counter = 0;\n+\n+\t/* Run the test suite */\n+\tret = clar_test(argc, argv);\n+\n+\t/* Your custom cleanup here */\n+\tcl_assert_equal_i(8, global_test_counter);\n+\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\nnew file mode 100644\nindex 00000000000..a4d91b72fa8\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c.sample\n@@ -0,0 +1,27 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Minimal main() for clar tests.\n+ *\n+ * Modify this with any application specific setup or teardown that you need.\n+ * The only required line is the call to `clar_test(argc, argv)`, which will\n+ * execute the test suite.  If you want to check the return value of the test\n+ * application, main() should return the same value returned by clar_test().\n+ */\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\t/* Run the test suite */\n+\treturn clar_test(argc, argv);\n+}\ndiff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\nnew file mode 100644\nindex 00000000000..220f4aa98a7\n--- /dev/null\n+++ b/t/unit-tests/clar/test/resources/test/file\n@@ -0,0 +1 @@\n+File\ndiff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\nnew file mode 100644\nindex 00000000000..faa1209262f\n--- /dev/null\n+++ b/t/unit-tests/clar/test/sample.c\n@@ -0,0 +1,84 @@\n+#include \"clar_test.h\"\n+#include <sys/stat.h>\n+\n+static int file_size(const char *filename)\n+{\n+\tstruct stat st;\n+\n+\tif (stat(filename, &st) == 0)\n+\t\treturn (int)st.st_size;\n+\treturn -1;\n+}\n+\n+void test_sample__initialize(void)\n+{\n+\tglobal_test_counter++;\n+}\n+\n+void test_sample__cleanup(void)\n+{\n+\tcl_fixture_cleanup(\"test\");\n+\n+\tcl_assert(file_size(\"test/file\") == -1);\n+}\n+\n+void test_sample__1(void)\n+{\n+\tcl_assert(1);\n+\tcl_must_pass(0);  /* 0 == success */\n+\tcl_must_fail(-1); /* <0 == failure */\n+\tcl_must_pass(-1); /* demonstrate a failing call */\n+}\n+\n+void test_sample__2(void)\n+{\n+\tcl_fixture_sandbox(\"test\");\n+\n+\tcl_assert(file_size(\"test/nonexistent\") == -1);\n+\tcl_assert(file_size(\"test/file\") > 0);\n+\tcl_assert(100 == 101);\n+}\n+\n+void test_sample__strings(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_s(\"expected\", actual);\n+\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n+\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n+}\n+\n+void test_sample__strings_with_length(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_strn(\"expected_\", actual, 8);\n+\tcl_assert_equal_strn(\"exactly\", actual, 2);\n+\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n+\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n+}\n+\n+void test_sample__int(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i(100, value);\n+\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n+}\n+\n+void test_sample__int_fmt(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n+}\n+\n+void test_sample__bool(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n+\tcl_assert_equal_b(0, value);\n+}\n+\n+void test_sample__ptr(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n+\tcl_assert_equal_p(&actual, actual);\n+}\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502006","messageId":"f24401f0a87059f4ff0c15ca0cfae4a899299b44.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 03/14] t/clar: fix compatibility with NonStop","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:41Z","receivedAt":"2024-09-03T09:14:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The NonStop platform does not have `mkdtemp()` available, which we rely\non in `build_sandbox_path()`. Fix this issue by using `mktemp()` and\n`mkdir()` instead on this platform.\n\nThis has been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/96\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 7c177f35258..e25057b7c49 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -120,6 +120,12 @@ static int build_sandbox_path(void)\n \tif (_mktemp(_clar_path) == NULL)\n \t\treturn -1;\n \n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(__TANDEM)\n+\tif (mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n \tif (mkdir(_clar_path, 0700) != 0)\n \t\treturn -1;\n #elif defined(_WIN32)\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502008","messageId":"658a601c5411f38457007ed961582b5d898f813a.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 04/14] clar: avoid compile error with mingw-w64","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:43Z","receivedAt":"2024-09-03T09:14:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen using mingw-w64 to compile the code, and using `_stat()`, it is\nnecessary to use `struct _stat`, too, and not `struct stat` (as the\nlatter is incompatible with the \"dashed\" version because it is limited\nto 32-bit time types for backwards compatibility).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nindex 3fc2c768158..e2ebe551d38 100644\n--- a/t/unit-tests/clar/clar.c\n+++ b/t/unit-tests/clar/clar.c\n@@ -68,7 +68,7 @@\n #\t\tdefine PRIxZ \"Ix\"\n #\tendif\n \n-#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+#\tif defined(_MSC_VER) || (defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))\n \ttypedef struct stat STAT_T;\n #\telse\n \ttypedef struct _stat STAT_T;\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502009","messageId":"0b8a6ac5fedf95e3d5328bf1fef75710e26ee3c3.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 05/14] clar(win32): avoid compile error due to unused `fs_copy()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:48Z","receivedAt":"2024-09-03T09:14:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen CLAR_FIXTURE_PATH is unset, the `fs_copy()` function seems not to\nbe used. But it is declared as `static`, and GCC does not like that,\ncomplaining that it should not be declared/defined to begin with.\n\nWe could mark this function as (potentially) unused by following the\n`MAYBE_UNUSED` pattern from Git's `git-compat-util.h`. However, this is\na GCC-only construct that is not understood by Visual C. Besides, `clar`\ndoes not use that pattern at all.\n\nInstead, let's use the `((void)SYMBOL);` pattern that `clar` already\nuses elsewhere; This avoids the compile error by sorta kinda make the\nfunction used after a fashion.\n\nNote: GCC 14.x (which Git for Windows' SDK already uses) is able to\nfigure out that this function is unused even though there are recursive\ncalls between `fs_copy()` and `fs_copydir_helper()`; Earlier GCC\nversions do not detect that, and therefore the issue has been hidden\nfrom the regular Linux CI builds (where GCC 14.x is not yet used). That\nis the reason why this change is only made in the Windows-specific\nportion of `t/unit-tests/clar/clar/fs.h`.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/fs.h | 2 ++\n 1 file changed, 2 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nindex 3e39890bd3e..8b206179fc4 100644\n--- a/t/unit-tests/clar/clar/fs.h\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -297,6 +297,8 @@ cl_fs_cleanup(void)\n {\n #ifdef CLAR_FIXTURE_PATH\n \tfs_rm(fixture_path(_clar_path, \"*\"));\n+#else\n+\t((void)fs_copy); /* unused */\n #endif\n }\n \n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502010","messageId":"c50e7a0ea68ba6fb4ef669403f79d3217d242f9f.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 06/14] clar: stop including `shellapi.h` unnecessarily","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:51Z","receivedAt":"2024-09-03T09:14:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThe `shellapi.h` header was included as of\nhttps://github.com/clar-test/clar/commit/136e763211aa, to have\n`SHFileOperation()` declared so that it could be called.\n\nHowever, https://github.com/clar-test/clar/commit/5ce31b69b525 removed\nthat call, and therefore that `#include <shellapi.h>` is unnecessary.\n\nIt is also unwanted in Git because this project uses a subset of Git for\nWindows' SDK in its CI builds that (for bandwidth reasons) excludes tons\nof header files, including `shellapi.h`.\n\nSo let's remove it.\n\nNote: Since the `windows.h` header would include `shellapi.h` anyway, we\nalso define `WIN32_LEAN_AND_MEAN` to avoid this and similar other\nunnecessary includes before including `windows.h`.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nindex e2ebe551d38..cef0f023c24 100644\n--- a/t/unit-tests/clar/clar.c\n+++ b/t/unit-tests/clar/clar.c\n@@ -19,9 +19,9 @@\n #include <sys/stat.h>\n \n #ifdef _WIN32\n+#\tdefine WIN32_LEAN_AND_MEAN\n #\tinclude <windows.h>\n #\tinclude <io.h>\n-#\tinclude <shellapi.h>\n #\tinclude <direct.h>\n \n #\tdefine _MAIN_CC __cdecl\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502011","messageId":"b8f3f16dd2776b889922f7d07ee5d448268fac65.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 07/14] Makefile: fix sparse dependency on GENERATED_H","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:54Z","receivedAt":"2024-09-03T09:14:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"check\" Makefile target is essentially an alias around the \"sparse\"\ntarget. The one difference though is that it will tell users to instead\nrun the \"test\" target in case they do not have sparse(1) installed, as\nchances are high that they wanted to execute the test suite rather than\ndoing semantic checks.\n\nBut even though the \"check\" target ultimately just ends up executing\n`make sparse`, it still depends on our generated headers. This does not\nmake any sense though: they are irrelevant for the \"test\" target advice,\nand if these headers are required for the \"sparse\" target they must be\ndeclared as a dependency on the aliased target, not the alias.\n\nBut even moving the dependency to the \"sparse\" target is wrong, as\nconcurrent builds may then end up generating the headers and running\nsparse concurrently. Instead, we make them a dependency of the specific\nobjects. While that is overly broad, it does ensure correct ordering.\nThe alternative, specifying which file depends on what generated header\nexplicitly, feels rather unmaintainable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 28742a60964..efd305ab358 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3254,7 +3254,7 @@ check-sha1:: t/helper/test-tool$X\n \n SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n \n-$(SP_OBJ): %.sp: %.c %.o\n+$(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n \t\t-Wsparse-error \\\n \t\t$(SPARSE_FLAGS) $(SP_EXTRA_FLAGS) $< && \\\n@@ -3295,7 +3295,7 @@ style:\n \tgit clang-format --style file --diff --extensions c,h\n \n .PHONY: check\n-check: $(GENERATED_H)\n+check:\n \t@if sparse; \\\n \tthen \\\n \t\techo >&2 \"Use 'make sparse' instead\"; \\\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502012","messageId":"3d3fe443b9a3f368287efa709cd744583da358e1.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 08/14] Makefile: make hdr-check depend on generated headers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:14:57Z","receivedAt":"2024-09-03T09:15:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"hdr-check\" Makefile target compiles each of our headers as a\nstandalone code unit to ensure that they are not missing any type\ndeclarations and can be included standalone.\n\nWith the next commit we will wire up the clar unit testing framework,\nwhich will have the effect that some headers start depending on\ngenerated ones. While we could declare that dependency explicitly, it\ndoes not really feel very maintainable in the future.\n\nInstead, we do the same as in the preceding commit and have the objects\ndepend on all of our generated headers. While again overly broad, it is\neasy to maintain and generating headers is not an expensive thing to do\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex efd305ab358..8c4487dd0c6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3284,7 +3284,7 @@ HCC = $(HCO:hco=hcc)\n \t@echo '#include \"git-compat-util.h\"' >$@\n \t@echo '#include \"$<\"' >>$@\n \n-$(HCO): %.hco: %.hcc FORCE\n+$(HCO): %.hco: %.hcc $(GENERATED_H) FORCE\n \t$(QUIET_HDR)$(CC) $(ALL_CFLAGS) -o /dev/null -c -xc $<\n \n .PHONY: hdr-check $(HCO)\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502013","messageId":"7d0f494850a48e5285e8823ff9252fdcf531439e.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 09/14] Makefile: do not use sparse on third-party sources","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:15:00Z","receivedAt":"2024-09-03T09:15:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have several third-party sources in our codebase that we have\nimported from upstream projects. These sources are mostly excluded from\nour static analysis, for example when running Coccinelle.\n\nDo the same for our \"sparse\" target by filtering them out.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 8c4487dd0c6..81a47b61327 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3252,7 +3252,8 @@ t/helper/test-%$X: t/helper/test-%.o GIT-LDFLAGS $(GITLIBS) $(REFTABLE_TEST_LIB)\n check-sha1:: t/helper/test-tool$X\n \tt/helper/test-sha1.sh\n \n-SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n+SP_SRC = $(filter-out $(THIRD_PARTY_SOURCES),$(patsubst %.o,%.c,$(OBJECTS)))\n+SP_OBJ = $(patsubst %.c,%.sp,$(SP_SRC))\n \n $(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502014","messageId":"9c74c5ae01989659c0347d2742f820d2161d274b.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 10/14] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:15:02Z","receivedAt":"2024-09-03T09:15:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Wire up the clar unit testing framework by introducing a new\n\"unit-tests\" executable. In contrast to the existing framework, this\nwill result in a single executable for all test suites. The ability to\npick specific tests to execute is retained via functionality built into\nthe clar itself.\n\nNote that we need to be a bit careful about how we need to invalidate\nour Makefile rules. While we obviously have to regenerate the clar suite\nwhen our test suites change, we also have to invalidate it in case any\nof the test suites gets removed. We do so by using our typical pattern\nof creating a `GIT-TEST-SUITES` file that gets updated whenever the set\nof test suites changes, so that we can easily depend on that file.\n\nAnother specialty is that we generate a \"clar-decls.h\" file. The test\nfunctions are neither static, nor do they have external declarations.\nThis is because they are getting parsed via \"generate.py\", which then\ncreates the external generations that get populated into an array. These\ndeclarations are only seen by the main function though.\n\nThe consequence is that we will get a bunch of \"missing prototypes\"\nerrors from our compiler for each of these test functions. To fix those\nerrors, we extract the `extern` declarations from \"clar.suite\" and put\nthem into a standalone header that then gets included by each of our\nunit tests. This gets rid of compiler warnings for every function which\nhas been extracted by \"generate.py\". More importantly though, it does\n_not_ get rid of warnings in case a function really isn't being used by\nanything. Thus, it would cause a compiler error if a function name was\nmistyped and thus not picked up by \"generate.py\".\n\nThe test driver \"unit-test.c\" is an empty stub for now. It will get\nimplemented in the next commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Makefile                       | 36 +++++++++++++++++++++---\n t/Makefile                     |  1 +\n t/unit-tests/.gitignore        |  2 ++\n t/unit-tests/clar-generate.awk | 50 ++++++++++++++++++++++++++++++++++\n t/unit-tests/unit-test.c       |  6 ++++\n t/unit-tests/unit-test.h       |  3 ++\n 7 files changed, 95 insertions(+), 4 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\ndiff --git a/.gitignore b/.gitignore\nindex 8caf3700c23..6687bd6db4c 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,6 +9,7 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n+/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex 81a47b61327..e38146b5eb0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -914,6 +914,8 @@ REFTABLE_TEST_LIB = reftable/libreftable_test.a\n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n GENERATED_H += hook-list.h\n+GENERATED_H += $(UNIT_TEST_DIR)/clar-decls.h\n+GENERATED_H += $(UNIT_TEST_DIR)/clar.suite\n \n .PHONY: generated-hdrs\n generated-hdrs: $(GENERATED_H)\n@@ -1334,6 +1336,11 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n+UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n+\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n@@ -2714,6 +2721,7 @@ OBJECTS += $(XDIFF_OBJS)\n OBJECTS += $(FUZZ_OBJS)\n OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n OBJECTS += $(UNIT_TEST_OBJS)\n+OBJECTS += $(UNIT_TESTS_OBJS)\n \n ifndef NO_CURL\n \tOBJECTS += http.o http-walker.o remote-curl.o\n@@ -3216,7 +3224,7 @@ endif\n \n test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))\n \n-all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)\n+all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n \n bin-wrappers/%: wrap-for-bin.sh\n \t$(call mkdir_p_parent_template)\n@@ -3648,7 +3656,7 @@ endif\n \n artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n \t\tGIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \\\n-\t\t$(UNIT_TEST_PROGS) $(MOFILES)\n+\t\t$(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) $(MOFILES)\n \t$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \\\n \t\tSHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'\n \ttest -n \"$(ARTIFACTS_DIRECTORY)\"\n@@ -3704,6 +3712,7 @@ cocciclean:\n \n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n+\t$(RM) GIT-TEST-SUITES\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3863,7 +3872,26 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n \n+GIT-TEST-SUITES: FORCE\n+\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n+\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n+\t\techo >&2 \"    * new test suites\"; \\\n+\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n+            fi\n+\n+$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n+\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n+\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n+\tdone >$@\n+$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n+\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n+$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR)\n+$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n+\t$(call mkdir_p_parent_template)\n+\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n+\n .PHONY: build-unit-tests unit-tests\n-build-unit-tests: $(UNIT_TEST_PROGS)\n-unit-tests: $(UNIT_TEST_PROGS) t/helper/test-tool$X\n+build-unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n+unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) t/helper/test-tool$X\n \t$(MAKE) -C t/ unit-tests\ndiff --git a/t/Makefile b/t/Makefile\nindex d2212de0b78..131ffd778fe 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -48,6 +48,7 @@ CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.tes\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n+UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n UNIT_TESTS = $(sort $(UNIT_TEST_PROGRAMS))\n UNIT_TESTS_NO_DIR = $(notdir $(UNIT_TESTS))\n \ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex 5e56e040ec0..d0632ec7f9e 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1 +1,3 @@\n /bin\n+/clar.suite\n+/clar-decls.h\ndiff --git a/t/unit-tests/clar-generate.awk b/t/unit-tests/clar-generate.awk\nnew file mode 100644\nindex 00000000000..ab71ce6c9fc\n--- /dev/null\n+++ b/t/unit-tests/clar-generate.awk\n@@ -0,0 +1,50 @@\n+function add_suite(suite, initialize, cleanup, count) {\n+       if (!suite) return\n+       suite_count++\n+       callback_count += count\n+       suites = suites \"    {\\n\"\n+       suites = suites \"        \\\"\" suite \"\\\",\\n\"\n+       suites = suites \"        \" initialize \",\\n\"\n+       suites = suites \"        \" cleanup \",\\n\"\n+       suites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n+       suites = suites \"    },\\n\"\n+}\n+\n+BEGIN {\n+       suites = \"static struct clar_suite _clar_suites[] = {\\n\"\n+}\n+\n+{\n+       print\n+       name = $3; sub(/\\(.*$/, \"\", name)\n+       suite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n+       short_name = name; sub(/^.*__/, \"\", short_name)\n+       cb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n+       if (suite != prev_suite) {\n+               add_suite(prev_suite, initialize, cleanup, count)\n+               if (callbacks) callbacks = callbacks \"};\\n\"\n+               callbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n+               initialize = \"{ NULL, NULL }\"\n+               cleanup = \"{ NULL, NULL }\"\n+               count = 0\n+               prev_suite = suite\n+       }\n+       if (short_name == \"initialize\") {\n+               initialize = cb\n+       } else if (short_name == \"cleanup\") {\n+               cleanup = cb\n+       } else {\n+               callbacks = callbacks \"    \" cb \",\\n\"\n+               count++\n+       }\n+}\n+\n+END {\n+       add_suite(suite, initialize, cleanup, count)\n+       suites = suites \"};\"\n+       if (callbacks) callbacks = callbacks \"};\"\n+       print callbacks\n+       print suites\n+       print \"static const size_t _clar_suite_count = \" suite_count \";\"\n+       print \"static const size_t _clar_callback_count = \" callback_count \";\"\n+}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nnew file mode 100644\nindex 00000000000..3d12cde6dae\n--- /dev/null\n+++ b/t/unit-tests/unit-test.c\n@@ -0,0 +1,6 @@\n+#include \"unit-test.h\"\n+\n+int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+{\n+\treturn 0;\n+}\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nnew file mode 100644\nindex 00000000000..66ec2387cc6\n--- /dev/null\n+++ b/t/unit-tests/unit-test.h\n@@ -0,0 +1,3 @@\n+#include \"git-compat-util.h\"\n+#include \"clar/clar.h\"\n+#include \"clar-decls.h\"\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502015","messageId":"8bd5b3e2b2989a30b597da2103eb8d9699cf3d7f.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 11/14] t/unit-tests: implement test driver","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:15:07Z","receivedAt":"2024-09-03T09:15:11Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The test driver in \"unit-test.c\" is responsible for setting up our unit\ntests and eventually running them. As such, it is also responsible for\nparsing the command line arguments.\n\nThe clar unit testing framework provides function `clar_test()` that\nparses command line arguments and then executes the tests for us. In\ntheory that would already be sufficient. We have the special requirement\nto always generate TAP-formatted output though, so we'd have to always\npass the \"-t\" argument to clar. Furthermore, some of the options exposed\nby clar are ineffective when \"-t\" is used, but they would still be shown\nwhen the user passes the \"-h\" parameter to have the clar show its usage.\n\nImplement our own option handling instead of using the one provided by\nclar, which gives us greater flexibility in how exactly we set things\nup.\n\nWe would ideally not use any \"normal\" code of ours for this such that\nthe unit testing framework doesn't depend on it working correctly. But\nit is somewhat dubious whether we really want to reimplement all of the\noption parsing. So for now, let's be pragmatic and reuse it until we\nfind a good reason in the future why we'd really want to avoid it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/unit-test.c | 43 ++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 41 insertions(+), 2 deletions(-)\n\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nindex 3d12cde6dae..96fa64de71d 100644\n--- a/t/unit-tests/unit-test.c\n+++ b/t/unit-tests/unit-test.c\n@@ -1,6 +1,45 @@\n #include \"unit-test.h\"\n+#include \"parse-options.h\"\n+#include \"string-list.h\"\n+#include \"strvec.h\"\n \n-int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+static const char * const unit_test_usage[] = {\n+\tN_(\"unit-test [<options>]\"),\n+\tNULL,\n+};\n+\n+int cmd_main(int argc, const char **argv)\n {\n-\treturn 0;\n+\tstruct string_list run_args = STRING_LIST_INIT_NODUP;\n+\tstruct string_list exclude_args = STRING_LIST_INIT_NODUP;\n+\tint immediate = 0;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL('i', \"--immediate\", &immediate,\n+\t\t\t N_(\"immediately exit upon the first failed test\")),\n+\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"name\"),\n+\t\t\t\tN_(\"run only test suite or individual test <name>\")),\n+\t\tOPT_STRING_LIST('x', \"exclude\", &exclude_args, N_(\"name\"),\n+\t\t\t\tN_(\"exclude test suite <name>\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct strvec args = STRVEC_INIT;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, NULL, options,\n+\t\t\t     unit_test_usage, PARSE_OPT_KEEP_ARGV0);\n+\tif (argc > 1)\n+\t\tusagef(_(\"extra command line parameter '%s'\"), argv[0]);\n+\n+\tstrvec_push(&args, argv[0]);\n+\tstrvec_push(&args, \"-t\");\n+\tfor (size_t i = 0; i < run_args.nr; i++)\n+\t\tstrvec_pushf(&args, \"-s%s\", run_args.items[i].string);\n+\tfor (size_t i = 0; i < exclude_args.nr; i++)\n+\t\tstrvec_pushf(&args, \"-x%s\", exclude_args.items[i].string);\n+\n+\tret = clar_test(args.nr, (char **) args.v);\n+\n+\tstring_list_clear(&run_args, 0);\n+\tstrvec_clear(&args);\n+\treturn ret;\n }\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502016","messageId":"3c3b9eacdfb5c5410b1ce868869564bcd4256013.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 12/14] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:15:10Z","receivedAt":"2024-09-03T09:15:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the strvec tests to use the new clar unit testing framework.\nThis is a first test balloon that demonstrates how the testing infra for\nclar-based tests looks like.\n\nThe tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\nrunning that binary with an injected error, it generates TAP output:\n\n    # ./t/unit-tests/bin/unit-tests\n    TAP version 13\n    # start of suite 1: strvec\n    ok 1 - strvec::init\n    ok 2 - strvec::dynamic_init\n    ok 3 - strvec::clear\n    not ok 4 - strvec::push\n        ---\n        reason: |\n          String mismatch: (&vec)->v[i] != expect[i]\n          'foo' != 'fo' (at byte 2)\n        at:\n          file: 't/unit-tests/strvec.c'\n          line: 48\n          function: 'test_strvec__push'\n        ---\n    ok 5 - strvec::pushf\n    ok 6 - strvec::pushl\n    ok 7 - strvec::pushv\n    ok 8 - strvec::replace_at_head\n    ok 9 - strvec::replace_at_tail\n    ok 10 - strvec::replace_in_between\n    ok 11 - strvec::replace_with_substring\n    ok 12 - strvec::remove_at_head\n    ok 13 - strvec::remove_at_tail\n    ok 14 - strvec::remove_in_between\n    ok 15 - strvec::pop_empty_array\n    ok 16 - strvec::pop_non_empty_array\n    ok 17 - strvec::split_empty_string\n    ok 18 - strvec::split_single_item\n    ok 19 - strvec::split_multiple_items\n    ok 20 - strvec::split_whitespace_only\n    ok 21 - strvec::split_multiple_consecutive_whitespaces\n    ok 22 - strvec::detach\n    1..22\n\nThe binary also supports some parameters that allow us to run only a\nsubset of unit tests or alter the output:\n\n    $ ./t/unit-tests/bin/unit-tests -h\n    Usage: ./t/unit-tests/bin/unit-tests [options]\n\n    Options:\n      -sname        Run only the suite with `name` (can go to individual test name)\n      -iname        Include the suite with `name`\n      -xname        Exclude the suite with `name`\n      -v            Increase verbosity (show suite names)\n      -q            Only report tests that had an error\n      -Q            Quit as soon as a test fails\n      -t            Display results in tap format\n      -l            Print suite names\n      -r[filename]  Write summary file (to the optional filename)\n\nFurthermore, running `make unit-tests` runs the binary along with all\nthe other unit tests we have.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                |   2 +-\n t/unit-tests/strvec.c   | 241 ++++++++++++++++++++++++++++++++++++++++\n t/unit-tests/t-strvec.c | 211 -----------------------------------\n 3 files changed, 242 insertions(+), 212 deletions(-)\n create mode 100644 t/unit-tests/strvec.c\n delete mode 100644 t/unit-tests/t-strvec.c\n\ndiff --git a/Makefile b/Makefile\nindex e38146b5eb0..56ce6c00e44 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n@@ -1356,7 +1357,6 @@ UNIT_TEST_PROGRAMS += t-reftable-record\n UNIT_TEST_PROGRAMS += t-reftable-tree\n UNIT_TEST_PROGRAMS += t-strbuf\n UNIT_TEST_PROGRAMS += t-strcmp-offset\n-UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-trailer\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\ndiff --git a/t/unit-tests/strvec.c b/t/unit-tests/strvec.c\nnew file mode 100644\nindex 00000000000..bf4c0cb172e\n--- /dev/null\n+++ b/t/unit-tests/strvec.c\n@@ -0,0 +1,241 @@\n+#include \"unit-test.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+#define check_strvec(vec, ...) \\\n+\tdo { \\\n+\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n+\t\tsize_t expect_len = ARRAY_SIZE(expect); \\\n+\t\tcl_assert(expect_len > 0); \\\n+\t\tcl_assert_equal_p(expect[expect_len - 1], NULL); \\\n+\t\tcl_assert_equal_i((vec)->nr, expect_len - 1); \\\n+\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n+\t\tfor (size_t i = 0; i < expect_len; i++) \\\n+\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n+\t} while (0)\n+\n+void test_strvec__init(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__dynamic_init(void)\n+{\n+\tstruct strvec vec;\n+\n+\tstrvec_init(&vec);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__clear(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\tstrvec_clear(&vec);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__push(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\n+\tstrvec_push(&vec, \"bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushf(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushf(&vec, \"foo: %d\", 1);\n+\tcheck_strvec(&vec, \"foo: 1\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushl(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushv(void)\n+{\n+\tconst char *strings[] = {\n+\t\t\"foo\", \"bar\", \"baz\", NULL,\n+\t};\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushv(&vec, strings);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 0, \"replaced\");\n+\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 2, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 1, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_with_substring(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", NULL);\n+\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n+\tcheck_strvec(&vec, \"oo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 0);\n+\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 2);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 1);\n+\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pop_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pop_non_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_empty_string(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \"\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_single_item(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_multiple_items(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \"foo bar baz\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_whitespace_only(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \" \\t\\n\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_multiple_consecutive_whitespaces(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \"foo\\n\\t bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__detach(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tconst char **detached;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\n+\tdetached = strvec_detach(&vec);\n+\tcl_assert_equal_s(detached[0], \"foo\");\n+\tcl_assert_equal_p(detached[1], NULL);\n+\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+\n+\tfree((char *) detached[0]);\n+\tfree(detached);\n+}\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\ndeleted file mode 100644\nindex c4bac8fc91b..00000000000\n--- a/t/unit-tests/t-strvec.c\n+++ /dev/null\n@@ -1,211 +0,0 @@\n-#include \"test-lib.h\"\n-#include \"strbuf.h\"\n-#include \"strvec.h\"\n-\n-#define check_strvec(vec, ...) \\\n-\tdo { \\\n-\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n-\t\t\t\t\t\t (uintmax_t)i); \\\n-\t\t\t\t\tbreak; \\\n-\t\t\t\t} \\\n-\t\t\t} \\\n-\t\t} \\\n-\t} while (0)\n-\n-int cmd_main(int argc, const char **argv)\n-{\n-\tif_test (\"static initialization\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"dynamic initialization\") {\n-\t\tstruct strvec vec;\n-\t\tstrvec_init(&vec);\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"clear\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_push(&vec, \"foo\");\n-\t\tstrvec_clear(&vec);\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"push\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\n-\t\tstrvec_push(&vec, \"foo\");\n-\t\tcheck_strvec(&vec, \"foo\", NULL);\n-\n-\t\tstrvec_push(&vec, \"bar\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushf\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushf(&vec, \"foo: %d\", 1);\n-\t\tcheck_strvec(&vec, \"foo: 1\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushl\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushv\") {\n-\t\tconst char *strings[] = {\n-\t\t\t\"foo\", \"bar\", \"baz\", NULL,\n-\t\t};\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\n-\t\tstrvec_pushv(&vec, strings);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace at head\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 0, \"replaced\");\n-\t\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace at tail\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 2, \"replaced\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace in between\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 1, \"replaced\");\n-\t\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace with substring\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", NULL);\n-\t\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n-\t\tcheck_strvec(&vec, \"oo\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove at head\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 0);\n-\t\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove at tail\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 2);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove in between\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 1);\n-\t\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pop with empty array\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pop(&vec);\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pop with non-empty array\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_pop(&vec);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split empty string\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"\");\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split single item\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo\");\n-\t\tcheck_strvec(&vec, \"foo\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split multiple items\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo bar baz\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split whitespace only\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \" \\t\\n\");\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split multiple consecutive whitespaces\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo\\n\\t bar\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"detach\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tconst char **detached;\n-\n-\t\tstrvec_push(&vec, \"foo\");\n-\n-\t\tdetached = strvec_detach(&vec);\n-\t\tcheck_str(detached[0], \"foo\");\n-\t\tcheck_pointer_eq(detached[1], NULL);\n-\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\n-\t\tfree((char *) detached[0]);\n-\t\tfree(detached);\n-\t}\n-\n-\treturn test_done();\n-}\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502017","messageId":"c8360db2f86e4ef50a7e98a6ae103082c3e2c5d5.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 13/14] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:15:12Z","receivedAt":"2024-09-03T09:15:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the ctype tests to use the new clar unit testing framework.\nIntroduce a new function `cl_failf()` that allows us to print a\nformatted error message, which we can use to point out which of the\ncharacters was classified incorrectly. This results in output like this\non failure:\n\n    # start of suite 1: ctype\n    not ok 1 - ctype::isspace\n        ---\n        reason: |\n          Test failed.\n          0x0d is classified incorrectly: expected 0, got 1\n        at:\n          file: 't/unit-tests/ctype.c'\n          line: 36\n          function: 'test_ctype__isspace'\n        ---\n    ok 2 - ctype::isdigit\n    ok 3 - ctype::isalpha\n    ok 4 - ctype::isalnum\n    ok 5 - ctype::is_glob_special\n    ok 6 - ctype::is_regex_special\n    ok 7 - ctype::is_pathspec_magic\n    ok 8 - ctype::isascii\n    ok 9 - ctype::islower\n    ok 10 - ctype::isupper\n    ok 11 - ctype::iscntrl\n    ok 12 - ctype::ispunct\n    ok 13 - ctype::isxdigit\n    ok 14 - ctype::isprint\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                            |  2 +-\n t/unit-tests/{t-ctype.c => ctype.c} | 71 +++++++++++++++++++++++++----\n t/unit-tests/unit-test.h            |  7 +++\n 3 files changed, 69 insertions(+), 11 deletions(-)\n rename t/unit-tests/{t-ctype.c => ctype.c} (68%)\n\ndiff --git a/Makefile b/Makefile\nindex 56ce6c00e44..c841cf70063 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,13 +1336,13 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += ctype\n UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n \n-UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n UNIT_TEST_PROGRAMS += t-hashmap\ndiff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/ctype.c\nsimilarity index 68%\nrename from t/unit-tests/t-ctype.c\nrename to t/unit-tests/ctype.c\nindex e28a7f50f9a..32e65867cdc 100644\n--- a/t/unit-tests/t-ctype.c\n+++ b/t/unit-tests/ctype.c\n@@ -1,16 +1,16 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n \n #define TEST_CHAR_CLASS(class, string) do { \\\n \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n-\tif_test (#class \" works\") { \\\n-\t\tfor (int i = 0; i < 256; i++) { \\\n-\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n-\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n-\t\t} \\\n-\t\tcheck(!class(EOF)); \\\n+\tfor (int i = 0; i < 256; i++) { \\\n+\t\tint actual = class(i), expect = !!memchr(string, i, len); \\\n+\t\tif (actual != expect) \\\n+\t\t\tcl_failf(\"0x%02x is classified incorrectly: expected %d, got %d\", \\\n+\t\t\t\t i, expect, actual); \\\n \t} \\\n+\tcl_assert(!class(EOF)); \\\n } while (0)\n \n #define DIGIT \"0123456789\"\n@@ -31,21 +31,72 @@\n \t\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\" \\\n \t\"\\x7f\"\n \n-int cmd_main(int argc, const char **argv) {\n+void test_ctype__isspace(void)\n+{\n \tTEST_CHAR_CLASS(isspace, \" \\n\\r\\t\");\n+}\n+\n+void test_ctype__isdigit(void)\n+{\n \tTEST_CHAR_CLASS(isdigit, DIGIT);\n+}\n+\n+void test_ctype__isalpha(void)\n+{\n \tTEST_CHAR_CLASS(isalpha, LOWER UPPER);\n+}\n+\n+void test_ctype__isalnum(void)\n+{\n \tTEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);\n+}\n+\n+void test_ctype__is_glob_special(void)\n+{\n \tTEST_CHAR_CLASS(is_glob_special, \"*?[\\\\\");\n+}\n+\n+void test_ctype__is_regex_special(void)\n+{\n \tTEST_CHAR_CLASS(is_regex_special, \"$()*+.?[\\\\^{|\");\n+}\n+\n+void test_ctype__is_pathspec_magic(void)\n+{\n \tTEST_CHAR_CLASS(is_pathspec_magic, \"!\\\"#%&',-/:;<=>@_`~\");\n+}\n+\n+void test_ctype__isascii(void)\n+{\n \tTEST_CHAR_CLASS(isascii, ASCII);\n+}\n+\n+void test_ctype__islower(void)\n+{\n \tTEST_CHAR_CLASS(islower, LOWER);\n+}\n+\n+void test_ctype__isupper(void)\n+{\n \tTEST_CHAR_CLASS(isupper, UPPER);\n+}\n+\n+void test_ctype__iscntrl(void)\n+{\n \tTEST_CHAR_CLASS(iscntrl, CNTRL);\n+}\n+\n+void test_ctype__ispunct(void)\n+{\n \tTEST_CHAR_CLASS(ispunct, PUNCT);\n+}\n+\n+void test_ctype__isxdigit(void)\n+{\n \tTEST_CHAR_CLASS(isxdigit, DIGIT \"abcdefABCDEF\");\n-\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n+}\n \n-\treturn test_done();\n+void test_ctype__isprint(void)\n+{\n+\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n }\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nindex 66ec2387cc6..85e5d6a948a 100644\n--- a/t/unit-tests/unit-test.h\n+++ b/t/unit-tests/unit-test.h\n@@ -1,3 +1,10 @@\n #include \"git-compat-util.h\"\n #include \"clar/clar.h\"\n #include \"clar-decls.h\"\n+#include \"strbuf.h\"\n+\n+#define cl_failf(fmt, ...) do { \\\n+\tchar desc[4096]; \\\n+\tsnprintf(desc, sizeof(desc), fmt, __VA_ARGS__); \\\n+\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n+} while (0)\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502018","messageId":"d51c146cd9d866b4900e0e75a4bf98ca45c720f5.1725349234.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"[PATCH v7 14/14] clar: add CMake support","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-03T09:15:15Z","receivedAt":"2024-09-03T09:15:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nNow that we're using `clar` as powerful test framework, we have to\nadjust the Visual C build (read: the CMake definition) to be able to\nhandle that, too.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n contrib/buildsystems/CMakeLists.txt | 53 +++++++++++++++++++++++++++++\n 1 file changed, 53 insertions(+)\n\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 832f46b316b..608fd3fe709 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -1004,6 +1004,59 @@ foreach(unit_test ${unit_test_PROGRAMS})\n \tendif()\n endforeach()\n \n+parse_makefile_for_scripts(unit_tests_SUITES \"UNIT_TESTS_SUITES\" \"\")\n+\n+set(clar_decls \"\")\n+set(clar_cbs \"\")\n+set(clar_cbs_count 0)\n+set(clar_suites \"static struct clar_suite _clar_suites[] = {\\n\")\n+list(LENGTH unit_tests_SUITES clar_suites_count)\n+foreach(suite ${unit_tests_SUITES})\n+\tfile(STRINGS \"${CMAKE_SOURCE_DIR}/t/unit-tests/${suite}.c\" decls\n+\t\tREGEX \"^void test_${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*\\\\(void\\\\)$\")\n+\n+\tlist(LENGTH decls decls_count)\n+\tstring(REGEX REPLACE \"void (test_${suite}__([a-zA-Z_0-9]*))\\\\(void\\\\)\" \"    { \\\"\\\\2\\\", &\\\\1 },\\n\" cbs ${decls})\n+\tstring(JOIN \"\" cbs ${cbs})\n+\tlist(TRANSFORM decls PREPEND \"extern \")\n+\tstring(JOIN \";\\n\" decls ${decls})\n+\n+\tstring(APPEND clar_decls \"${decls};\\n\")\n+\tstring(APPEND clar_cbs\n+\t\t\"static const struct clar_func _clar_cb_${suite}[] = {\\n\"\n+\t\t${cbs}\n+\t\t\"};\\n\")\n+\tstring(APPEND clar_suites\n+\t\t\"    {\\n\"\n+\t\t\"        \\\"${suite}\\\",\\n\"\n+\t\t\"        { NULL, NULL },\\n\"\n+\t\t\"        { NULL, NULL },\\n\"\n+\t\t\"        _clar_cb_${suite}, ${decls_count}, 1\\n\"\n+\t\t\"    },\\n\")\n+\tmath(EXPR clar_cbs_count \"${clar_cbs_count}+${decls_count}\")\n+endforeach()\n+string(APPEND clar_suites\n+\t\"};\\n\"\n+\t\"static const size_t _clar_suite_count = ${clar_suites_count};\\n\"\n+\t\"static const size_t _clar_callback_count = ${clar_cbs_count};\\n\")\n+file(WRITE \"${CMAKE_BINARY_DIR}/t/unit-tests/clar-decls.h\" \"${clar_decls}\")\n+file(WRITE \"${CMAKE_BINARY_DIR}/t/unit-tests/clar.suite\" \"${clar_decls}\" \"${clar_cbs}\" \"${clar_suites}\")\n+\n+list(TRANSFORM unit_tests_SUITES PREPEND \"${CMAKE_SOURCE_DIR}/t/unit-tests/\")\n+list(TRANSFORM unit_tests_SUITES APPEND \".c\")\n+add_library(unit-tests-lib ${unit_tests_SUITES} \"${CMAKE_SOURCE_DIR}/t/unit-tests/clar/clar.c\")\n+target_include_directories(unit-tests-lib PRIVATE \"${CMAKE_SOURCE_DIR}/t/unit-tests\")\n+add_executable(unit-tests \"${CMAKE_SOURCE_DIR}/t/unit-tests/unit-test.c\")\n+target_link_libraries(unit-tests unit-tests-lib common-main)\n+set_target_properties(unit-tests\n+\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+if(MSVC)\n+\tset_target_properties(unit-tests\n+\t\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+\tset_target_properties(unit-tests\n+\t\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+endif()\n+\n #test-tool\n parse_makefile_for_sources(test-tool_SOURCES \"TEST_BUILTINS_OBJS\")\n add_library(test-lib OBJECT ${CMAKE_SOURCE_DIR}/t/unit-tests/test-lib.c)\n-- \n2.46.0.421.g159f2d50e7.dirty\n\n"},{"id":"502020","messageId":"CAPig+cRrwGsGA+2B5+eYpQY-TdGsDspTgviDgwZ+Zx_sAknZzA@mail.gmail.com","threadId":"61874","inReplyTo":"55a9b46e65f2072ca7994b042dfe24d1c153c334.1725349234.git.ps@pks.im","subject":"Re: [PATCH v7 02/14] t: import the clar unit testing framework","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2024-09-03T09:47:04Z","receivedAt":"2024-09-03T09:47:16Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Tue, Sep 3, 2024 at 5:15 AM Patrick Steinhardt <ps@pks.im> wrote:>\n> Our unit testing framework is a homegrown solution. While it supports\n> most of our needs, it is likely that the volume of unit tests will grow\n> quite a bit in the future such that we can exercise low-level subsystems\n> directly. This surfaces several shortcomings that the current solution\n> has:\n>\n>   - There is no way to run only one specific tests. While some of our\n>     unit tests wire this up manually, others don't. In general, it\n>     requires quite a bit of boilerplate to get this set up correctly.\n>\n>   - Failures do not cause a test to stop execution directly. Instead,\n>     the test author needs to return manually whenever an assertion\n>     fails. This is rather verbose and is not done correctly in most of\n>     our unit tests.\n>\n>   - Wiring up a new testcase requires both implementing the test\n>     function and calling it in the respective test suite's main\n>     function, which is creating code duplication.\n>\n> We can of course fix all of these issues ourselves, but that feels\n> rather pointless when there are already so many unit testing frameworks\n> out there that have those features.\n>\n> We line out some requirements for any unit testing framework in\n\nPerhaps you meant s/line out/outline/ ?\n\n(Not worth a reroll.)\n\n> \"Documentation/technical/unit-tests.txt\". The \"clar\" unit testing\n> framework, which isn't listed in that table yet, ticks many of the\n> boxes:\n>\n>   - It is licensed under ISC, which is compatible.\n>\n>   - It is easily vendorable because it is rather tiny at around 1200\n>     lines of code.\n>\n>   - It is easily hackable due to the same reason.\n>\n>   - It has TAP support.\n>\n>   - It has skippable tests.\n>\n>   - It preprocesses test files in order to extract test functions, which\n>     then get wired up automatically.\n>\n> While it's not perfect, the fact that clar originates from the libgit2\n> project means that it should be rather easy for us to collaborate with\n> upstream to plug any gaps.\n>\n> Import the clar unit testing framework at commit 1516124 (Merge pull\n> request #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\n> will be wired up in subsequent commits.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n"},{"id":"502021","messageId":"3be1d990-9f9c-4963-bce3-742511b79022@gmail.com","threadId":"61874","inReplyTo":"Zta-nU4UMyrWgABW@tanuki","subject":"Re: [PATCH v6 11/13] t/unit-tests: convert strvec tests to use clar","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-09-03T09:48:01Z","receivedAt":"2024-09-03T09:48:04Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 03/09/2024 08:45, Patrick Steinhardt wrote:\n> On Wed, Aug 28, 2024 at 02:17:05PM +0100, Phillip Wood wrote:\n>> Hi Patrick\n>>\n>> On 20/08/2024 15:02, Patrick Steinhardt wrote:\n>> It might be a good opportunity to show the set-up and tear-down facilities\n>> in clar as well instead of repeating the initialization in each test.\n> \n> I don't think it's a good fit here, as setup and teardown would hit the\n> system under test. I rather think they should be used in cases where you\n> e.g. always have to setup a repository for your tests.\n\nI'm not sure I follow. I was suggesting we define \ntest_strvec__initialize() to initialize a global strvec which the tests \nuse and is then freed by test_strvec__cleanup() like the tests/adding.c \nexample the clar's README.md. That would allow use to remove the setup \nand teardown from each test. As I understand it clar's setup/cleanup \nfunctionality is usable without setting up a sandbox directory for each \ntest.\n\nI'll take a look at v7 in the next few days - I suspect we're getting to \nthe point where it's ready to be merged.\n\nBest Wishes\n\nPhillip\n\n> Patrick\n"},{"id":"502090","messageId":"ZtgAJFSXn2M252ZY@pks.im","threadId":"61874","inReplyTo":"3be1d990-9f9c-4963-bce3-742511b79022@gmail.com","subject":"Re: [PATCH v6 11/13] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T06:37:30Z","receivedAt":"2024-09-04T06:37:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Sep 03, 2024 at 10:48:01AM +0100, phillip.wood123@gmail.com wrote:\n> Hi Patrick\n> \n> On 03/09/2024 08:45, Patrick Steinhardt wrote:\n> > On Wed, Aug 28, 2024 at 02:17:05PM +0100, Phillip Wood wrote:\n> > > Hi Patrick\n> > > \n> > > On 20/08/2024 15:02, Patrick Steinhardt wrote:\n> > > It might be a good opportunity to show the set-up and tear-down facilities\n> > > in clar as well instead of repeating the initialization in each test.\n> > \n> > I don't think it's a good fit here, as setup and teardown would hit the\n> > system under test. I rather think they should be used in cases where you\n> > e.g. always have to setup a repository for your tests.\n> \n> I'm not sure I follow. I was suggesting we define test_strvec__initialize()\n> to initialize a global strvec which the tests use and is then freed by\n> test_strvec__cleanup() like the tests/adding.c example the clar's README.md.\n> That would allow use to remove the setup and teardown from each test. As I\n> understand it clar's setup/cleanup functionality is usable without setting\n> up a sandbox directory for each test.\n\nWhat I'm saying is that `strvec_init()` itself is part of the system\nunder test, so evicting that into a `__initialize()` function doesn't\nquite make sense to me. If there was for example a bug somewhere in the\nstrvec code we might bring the global `struct strvec` into a state that\nis completely unusable, and thus all subsequent tests would fail. We\ncould of course work around that by always zeroing out the struct, but\nbecause of that I just don't think it's a good fit.\n\nI rather see the usefulness of `__initialize()` in setting up auxiliary\ndata structures that are a dependency of the system under test, but\nwhich are not the system under test itself.\n\n> I'll take a look at v7 in the next few days - I suspect we're getting to the\n> point where it's ready to be merged.\n\nThanks!\n"},{"id":"502091","messageId":"ZtgAcOfOjVDyoI4r@pks.im","threadId":"61874","inReplyTo":"CAPig+cRrwGsGA+2B5+eYpQY-TdGsDspTgviDgwZ+Zx_sAknZzA@mail.gmail.com","subject":"Re: [PATCH v7 02/14] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T06:38:40Z","receivedAt":"2024-09-04T06:38:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Sep 03, 2024 at 05:47:04AM -0400, Eric Sunshine wrote:\n> On Tue, Sep 3, 2024 at 5:15 AM Patrick Steinhardt <ps@pks.im> wrote:>\n> > Our unit testing framework is a homegrown solution. While it supports\n> > most of our needs, it is likely that the volume of unit tests will grow\n> > quite a bit in the future such that we can exercise low-level subsystems\n> > directly. This surfaces several shortcomings that the current solution\n> > has:\n> >\n> >   - There is no way to run only one specific tests. While some of our\n> >     unit tests wire this up manually, others don't. In general, it\n> >     requires quite a bit of boilerplate to get this set up correctly.\n> >\n> >   - Failures do not cause a test to stop execution directly. Instead,\n> >     the test author needs to return manually whenever an assertion\n> >     fails. This is rather verbose and is not done correctly in most of\n> >     our unit tests.\n> >\n> >   - Wiring up a new testcase requires both implementing the test\n> >     function and calling it in the respective test suite's main\n> >     function, which is creating code duplication.\n> >\n> > We can of course fix all of these issues ourselves, but that feels\n> > rather pointless when there are already so many unit testing frameworks\n> > out there that have those features.\n> >\n> > We line out some requirements for any unit testing framework in\n> \n> Perhaps you meant s/line out/outline/ ?\n> \n> (Not worth a reroll.)\n\nHum. Yeah, this is German grammar leaking into English. Anyway, TIL what\n\"to line out\" means :) Thanks!\n\nPatrick\n"},{"id":"502107","messageId":"33243592-1342-4a11-a592-63b35b0abe9d@gmail.com","threadId":"61874","inReplyTo":"ZtgAJFSXn2M252ZY@pks.im","subject":"Re: [PATCH v6 11/13] t/unit-tests: convert strvec tests to use clar","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-09-04T09:31:06Z","receivedAt":"2024-09-04T09:31:10Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 04/09/2024 07:37, Patrick Steinhardt wrote:\n> On Tue, Sep 03, 2024 at 10:48:01AM +0100, phillip.wood123@gmail.com wrote:\n>> On 03/09/2024 08:45, Patrick Steinhardt wrote:\n>>> On Wed, Aug 28, 2024 at 02:17:05PM +0100, Phillip Wood wrote:\n>>>> On 20/08/2024 15:02, Patrick Steinhardt wrote:\n>>>> It might be a good opportunity to show the set-up and tear-down facilities\n>>>> in clar as well instead of repeating the initialization in each test.\n>>>\n>>> I don't think it's a good fit here, as setup and teardown would hit the\n>>> system under test. I rather think they should be used in cases where you\n>>> e.g. always have to setup a repository for your tests.\n>>\n>> I'm not sure I follow. I was suggesting we define test_strvec__initialize()\n>> to initialize a global strvec which the tests use and is then freed by\n>> test_strvec__cleanup() like the tests/adding.c example the clar's README.md.\n>> That would allow use to remove the setup and teardown from each test. As I\n>> understand it clar's setup/cleanup functionality is usable without setting\n>> up a sandbox directory for each test.\n> \n> What I'm saying is that `strvec_init()` itself is part of the system\n> under test,\n\nOh, I see - I'd misunderstood what you meant by \"system under test\"\n\n> so evicting that into a `__initialize()` function doesn't\n> quite make sense to me. If there was for example a bug somewhere in the\n> strvec code we might bring the global `struct strvec` into a state that\n> is completely unusable, and thus all subsequent tests would fail. We\n> could of course work around that by always zeroing out the struct, but\n> because of that I just don't think it's a good fit.\n\nIsn't the point of strvec_init() to ensure that the `struct strvec` \npassed to it is usable? If it does not do that then having all the tests \nfail is good as it tells us there is a bug. I was hoping that \n__initialize() would be a useful replacement for the\n\n\tTEST(setup(test_fn()), \"description\")\n\npattern in some of our existing tests. I find hoisting the common \ninitialization and cleanup out of each tests makes them easier to review \nas I can review the setup code once and not worry about it when \nreviewing each test. It also avoids a lot of repetition when writing \ntests and keeps the test bodies concise.\n\n> I rather see the usefulness of `__initialize()` in setting up auxiliary\n> data structures that are a dependency of the system under test, but\n> which are not the system under test itself.\n\nThat's certainly a good use for them\n\nBest Wishes\n\nPhillip\n\n>> I'll take a look at v7 in the next few days - I suspect we're getting to the\n>> point where it's ready to be merged.\n> \n> Thanks!\n"},{"id":"502110","messageId":"e435e05a-376c-4928-9852-6925df855399@gmail.com","threadId":"61874","inReplyTo":"8bd5b3e2b2989a30b597da2103eb8d9699cf3d7f.1725349234.git.ps@pks.im","subject":"Re: [PATCH v7 11/14] t/unit-tests: implement test driver","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-09-04T13:35:20Z","receivedAt":"2024-09-04T13:35:24Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 03/09/2024 10:15, Patrick Steinhardt wrote:\n> The test driver in \"unit-test.c\" is responsible for setting up our unit\n> tests and eventually running them. As such, it is also responsible for\n> parsing the command line arguments.\n> \n> The clar unit testing framework provides function `clar_test()` that\n> parses command line arguments and then executes the tests for us. In\n> theory that would already be sufficient. We have the special requirement\n> to always generate TAP-formatted output though, so we'd have to always\n> pass the \"-t\" argument to clar. Furthermore, some of the options exposed\n> by clar are ineffective when \"-t\" is used, but they would still be shown\n> when the user passes the \"-h\" parameter to have the clar show its usage.\n> \n> Implement our own option handling instead of using the one provided by\n> clar, which gives us greater flexibility in how exactly we set things\n> up.\n\nThat makes sense\n\n> We would ideally not use any \"normal\" code of ours for this such that\n> the unit testing framework doesn't depend on it working correctly. But\n> it is somewhat dubious whether we really want to reimplement all of the\n> option parsing. So for now, let's be pragmatic and reuse it until we\n> find a good reason in the future why we'd really want to avoid it.\n\nI think that's fine for now. Using parse_options() gives a much nicer \nuser experience than clar_test() as it supports long options and has \nmore flexible support for option arguments. I'd expect the code that \nimplements \"struct string_list\" and \"struct strvec\" to be pretty stable \nso its probably safe to rely on those.\n\nGiven there's only a couple of options it wouldn't be too bad to \nimplement the parsing ourselves if we have to in the future. We might \nneed to do that to support the libification work as I suspect we wont \nwant to link tests for other libraries against libgit.a.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>   t/unit-tests/unit-test.c | 43 ++++++++++++++++++++++++++++++++++++++--\n>   1 file changed, 41 insertions(+), 2 deletions(-)\n> \n> diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\n> index 3d12cde6dae..96fa64de71d 100644\n> --- a/t/unit-tests/unit-test.c\n> +++ b/t/unit-tests/unit-test.c\n> @@ -1,6 +1,45 @@\n>   #include \"unit-test.h\"\n> +#include \"parse-options.h\"\n> +#include \"string-list.h\"\n> +#include \"strvec.h\"\n>   \n> -int cmd_main(int argc UNUSED, const char **argv UNUSED)\n> +static const char * const unit_test_usage[] = {\n> +\tN_(\"unit-test [<options>]\"),\n> +\tNULL,\n> +};\n> +\n> +int cmd_main(int argc, const char **argv)\n>   {\n> -\treturn 0;\n> +\tstruct string_list run_args = STRING_LIST_INIT_NODUP;\n> +\tstruct string_list exclude_args = STRING_LIST_INIT_NODUP;\n> +\tint immediate = 0;\n> +\tstruct option options[] = {\n> +\t\tOPT_BOOL('i', \"--immediate\", &immediate,\n> +\t\t\t N_(\"immediately exit upon the first failed test\")),\n\nThis is unused. If we want to to behave like the \"--immediate\" option of \nour integration tests that's  hard to implement by wrapping clar_test() \nwhich requires \"-i<suite>\". The simplest thing would be to just drop it \nfor now. Otherwise as the most likely use for \"-i\" is manually testing \nsome tests in a suite we could require \"--run\" with \"-i\". Then we would \nhave one or more suite names which we can append to \"-i\" when passing it \nto clar_test(). Alternatively we could include \"clar.suite\" and wade \nthrough all the test suite names ourselves to construct a suitable list \nof \"-i\" options to pass to clar_test() but that would probably mean we \nhave to parse the excludes as well which makes it all a bit of a faff.\n\n> +\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"name\"),\n> +\t\t\t\tN_(\"run only test suite or individual test <name>\")),\n\nIt's nice that this option name now matches our integration tests. It \nwould be helpful to show the syntax for \"name\" (I think it expects \n<suite>[::<test>]) but I failed to come up with a concise description to \nadd to the help here.\n\nBest Wishes\n\nPhillip\n\n> +\t\tOPT_STRING_LIST('x', \"exclude\", &exclude_args, N_(\"name\"),\n> +\t\t\t\tN_(\"exclude test suite <name>\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct strvec args = STRVEC_INIT;\n> +\tint ret;\n> +\n> +\targc = parse_options(argc, argv, NULL, options,\n> +\t\t\t     unit_test_usage, PARSE_OPT_KEEP_ARGV0);\n> +\tif (argc > 1)\n> +\t\tusagef(_(\"extra command line parameter '%s'\"), argv[0]);\n> +\n> +\tstrvec_push(&args, argv[0]);\n> +\tstrvec_push(&args, \"-t\");\n> +\tfor (size_t i = 0; i < run_args.nr; i++)\n> +\t\tstrvec_pushf(&args, \"-s%s\", run_args.items[i].string);\n> +\tfor (size_t i = 0; i < exclude_args.nr; i++)\n> +\t\tstrvec_pushf(&args, \"-x%s\", exclude_args.items[i].string);\n> +\n> +\tret = clar_test(args.nr, (char **) args.v);\n> +\n> +\tstring_list_clear(&run_args, 0);\n> +\tstrvec_clear(&args);\n> +\treturn ret;\n>   }\n"},{"id":"502111","messageId":"b177ffce-f5cb-4df9-8b81-79128f9185d0@gmail.com","threadId":"61874","inReplyTo":"cover.1725349234.git.ps@pks.im","subject":"Re: [PATCH v7 00/14] Introduce clar testing framework","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-09-04T13:35:33Z","receivedAt":"2024-09-04T13:35:37Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 03/09/2024 10:14, Patrick Steinhardt wrote:\n> Hi,\n> \n> this is the seventh version fo my patch series that introduces the clar\n> unit testing framework.\n> \n> Changes compared to v6:\n> \n>    - Add some explanations for why we introduce clar in the first place\n>      to the second commit message.\n> \n>    - Split out implementation of the test driver, that is the main\n>      function, into a separate commit.\n> \n>    - Reimplement argument parsing ourselves. This allows more flexibility\n>      and gets rid of some options that do not make sense for us.\n> \n>    - Add an empty line between declarations and code.\n> \n>    - Improve the test messages in ctype tests to also mention our\n>      expectations.\n> \n>    - Adapt `cl_failf()` to not use `xstrfmt()`, but `snprintf()` with a\n>      static buffer instead.\n\nI've left some comments on patch 11 which is new. Apart from that \nrange-diff looks fine to me.\n\nThanks for working on this\n\nPhillip\n\n> Thanks!\n> \n> Patrick\n> \n> Johannes Schindelin (4):\n>    clar: avoid compile error with mingw-w64\n>    clar(win32): avoid compile error due to unused `fs_copy()`\n>    clar: stop including `shellapi.h` unnecessarily\n>    clar: add CMake support\n> \n> Patrick Steinhardt (10):\n>    t: do not pass GIT_TEST_OPTS to unit tests with prove\n>    t: import the clar unit testing framework\n>    t/clar: fix compatibility with NonStop\n>    Makefile: fix sparse dependency on GENERATED_H\n>    Makefile: make hdr-check depend on generated headers\n>    Makefile: do not use sparse on third-party sources\n>    Makefile: wire up the clar unit testing framework\n>    t/unit-tests: implement test driver\n>    t/unit-tests: convert strvec tests to use clar\n>    t/unit-tests: convert ctype tests to use clar\n> \n>   .gitignore                                 |   1 +\n>   Documentation/technical/unit-tests.txt     |   2 +\n>   Makefile                                   |  53 +-\n>   contrib/buildsystems/CMakeLists.txt        |  53 ++\n>   t/Makefile                                 |   4 +-\n>   t/run-test.sh                              |   2 +-\n>   t/unit-tests/.gitignore                    |   2 +\n>   t/unit-tests/clar-generate.awk             |  50 ++\n>   t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n>   t/unit-tests/clar/COPYING                  |  15 +\n>   t/unit-tests/clar/README.md                | 329 ++++++++\n>   t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n>   t/unit-tests/clar/clar.h                   | 173 +++++\n>   t/unit-tests/clar/clar/fixtures.h          |  50 ++\n>   t/unit-tests/clar/clar/fs.h                | 524 +++++++++++++\n>   t/unit-tests/clar/clar/print.h             | 211 ++++++\n>   t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n>   t/unit-tests/clar/clar/summary.h           | 143 ++++\n>   t/unit-tests/clar/generate.py              | 266 +++++++\n>   t/unit-tests/clar/test/.gitignore          |   4 +\n>   t/unit-tests/clar/test/Makefile            |  39 +\n>   t/unit-tests/clar/test/clar_test.h         |  16 +\n>   t/unit-tests/clar/test/main.c              |  40 +\n>   t/unit-tests/clar/test/main.c.sample       |  27 +\n>   t/unit-tests/clar/test/resources/test/file |   1 +\n>   t/unit-tests/clar/test/sample.c            |  84 ++\n>   t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n>   t/unit-tests/strvec.c                      | 241 ++++++\n>   t/unit-tests/t-strvec.c                    | 211 ------\n>   t/unit-tests/unit-test.c                   |  45 ++\n>   t/unit-tests/unit-test.h                   |  10 +\n>   31 files changed, 3457 insertions(+), 234 deletions(-)\n>   create mode 100644 t/unit-tests/clar-generate.awk\n>   create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n>   create mode 100644 t/unit-tests/clar/COPYING\n>   create mode 100644 t/unit-tests/clar/README.md\n>   create mode 100644 t/unit-tests/clar/clar.c\n>   create mode 100644 t/unit-tests/clar/clar.h\n>   create mode 100644 t/unit-tests/clar/clar/fixtures.h\n>   create mode 100644 t/unit-tests/clar/clar/fs.h\n>   create mode 100644 t/unit-tests/clar/clar/print.h\n>   create mode 100644 t/unit-tests/clar/clar/sandbox.h\n>   create mode 100644 t/unit-tests/clar/clar/summary.h\n>   create mode 100755 t/unit-tests/clar/generate.py\n>   create mode 100644 t/unit-tests/clar/test/.gitignore\n>   create mode 100644 t/unit-tests/clar/test/Makefile\n>   create mode 100644 t/unit-tests/clar/test/clar_test.h\n>   create mode 100644 t/unit-tests/clar/test/main.c\n>   create mode 100644 t/unit-tests/clar/test/main.c.sample\n>   create mode 100644 t/unit-tests/clar/test/resources/test/file\n>   create mode 100644 t/unit-tests/clar/test/sample.c\n>   rename t/unit-tests/{t-ctype.c => ctype.c} (68%)\n>   create mode 100644 t/unit-tests/strvec.c\n>   delete mode 100644 t/unit-tests/t-strvec.c\n>   create mode 100644 t/unit-tests/unit-test.c\n>   create mode 100644 t/unit-tests/unit-test.h\n> \n> Range-diff against v6:\n>   1:  e48a6461137 =  1:  b67f10ec0b0 t: do not pass GIT_TEST_OPTS to unit tests with prove\n>   2:  1710e9f9ff7 !  2:  55a9b46e65f t: import the clar unit testing framework\n>      @@ Metadata\n>        ## Commit message ##\n>           t: import the clar unit testing framework\n>       \n>      +    Our unit testing framework is a homegrown solution. While it supports\n>      +    most of our needs, it is likely that the volume of unit tests will grow\n>      +    quite a bit in the future such that we can exercise low-level subsystems\n>      +    directly. This surfaces several shortcomings that the current solution\n>      +    has:\n>      +\n>      +      - There is no way to run only one specific tests. While some of our\n>      +        unit tests wire this up manually, others don't. In general, it\n>      +        requires quite a bit of boilerplate to get this set up correctly.\n>      +\n>      +      - Failures do not cause a test to stop execution directly. Instead,\n>      +        the test author needs to return manually whenever an assertion\n>      +        fails. This is rather verbose and is not done correctly in most of\n>      +        our unit tests.\n>      +\n>      +      - Wiring up a new testcase requires both implementing the test\n>      +        function and calling it in the respective test suite's main\n>      +        function, which is creating code duplication.\n>      +\n>      +    We can of course fix all of these issues ourselves, but that feels\n>      +    rather pointless when there are already so many unit testing frameworks\n>      +    out there that have those features.\n>      +\n>      +    We line out some requirements for any unit testing framework in\n>      +    \"Documentation/technical/unit-tests.txt\". The \"clar\" unit testing\n>      +    framework, which isn't listed in that table yet, ticks many of the\n>      +    boxes:\n>      +\n>      +      - It is licensed under ISC, which is compatible.\n>      +\n>      +      - It is easily vendorable because it is rather tiny at around 1200\n>      +        lines of code.\n>      +\n>      +      - It is easily hackable due to the same reason.\n>      +\n>      +      - It has TAP support.\n>      +\n>      +      - It has skippable tests.\n>      +\n>      +      - It preprocesses test files in order to extract test functions, which\n>      +        then get wired up automatically.\n>      +\n>      +    While it's not perfect, the fact that clar originates from the libgit2\n>      +    project means that it should be rather easy for us to collaborate with\n>      +    upstream to plug any gaps.\n>      +\n>           Import the clar unit testing framework at commit 1516124 (Merge pull\n>           request #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\n>           will be wired up in subsequent commits.\n>   3:  5c21aa87aa2 =  3:  f24401f0a87 t/clar: fix compatibility with NonStop\n>   4:  06d2bce0d82 =  4:  658a601c541 clar: avoid compile error with mingw-w64\n>   5:  f88b3421a09 =  5:  0b8a6ac5fed clar(win32): avoid compile error due to unused `fs_copy()`\n>   6:  5fb4c55be33 =  6:  c50e7a0ea68 clar: stop including `shellapi.h` unnecessarily\n>   7:  e0dcbd5ca83 =  7:  b8f3f16dd27 Makefile: fix sparse dependency on GENERATED_H\n>   8:  77a03f8df70 =  8:  3d3fe443b9a Makefile: make hdr-check depend on generated headers\n>   9:  c91dd7327e3 =  9:  7d0f494850a Makefile: do not use sparse on third-party sources\n> 10:  115c15aa9ae ! 10:  9c74c5ae019 Makefile: wire up the clar unit testing framework\n>      @@ Commit message\n>           anything. Thus, it would cause a compiler error if a function name was\n>           mistyped and thus not picked up by \"generate.py\".\n>       \n>      +    The test driver \"unit-test.c\" is an empty stub for now. It will get\n>      +    implemented in the next commit.\n>      +\n>           Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>       \n>        ## .gitignore ##\n>      @@ t/unit-tests/unit-test.c (new)\n>       @@\n>       +#include \"unit-test.h\"\n>       +\n>      -+int cmd_main(int argc, const char **argv)\n>      ++int cmd_main(int argc UNUSED, const char **argv UNUSED)\n>       +{\n>      -+\tconst char **argv_copy;\n>      -+\tint ret;\n>      -+\n>      -+\t/* Append the \"-t\" flag such that the tests generate TAP output. */\n>      -+\tALLOC_ARRAY(argv_copy, argc + 2);\n>      -+\tCOPY_ARRAY(argv_copy, argv, argc);\n>      -+\targv_copy[argc++] = \"-t\";\n>      -+\targv_copy[argc] = NULL;\n>      -+\n>      -+\tret = clar_test(argc, (char **) argv_copy);\n>      -+\n>      -+\tfree(argv_copy);\n>      -+\treturn ret;\n>      ++\treturn 0;\n>       +}\n>       \n>        ## t/unit-tests/unit-test.h (new) ##\n>   -:  ----------- > 11:  8bd5b3e2b29 t/unit-tests: implement test driver\n> 11:  b3b8df04872 ! 12:  3c3b9eacdfb t/unit-tests: convert strvec tests to use clar\n>      @@ Commit message\n>           clar-based tests looks like.\n>       \n>           The tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\n>      -    running that binary, it generates TAP output:\n>      +    running that binary with an injected error, it generates TAP output:\n>       \n>               # ./t/unit-tests/bin/unit-tests\n>               TAP version 13\n>      @@ Commit message\n>               ok 1 - strvec::init\n>               ok 2 - strvec::dynamic_init\n>               ok 3 - strvec::clear\n>      -        ok 4 - strvec::push\n>      -        ok 5 - strvec::pushft_pushf\n>      +        not ok 4 - strvec::push\n>      +            ---\n>      +            reason: |\n>      +              String mismatch: (&vec)->v[i] != expect[i]\n>      +              'foo' != 'fo' (at byte 2)\n>      +            at:\n>      +              file: 't/unit-tests/strvec.c'\n>      +              line: 48\n>      +              function: 'test_strvec__push'\n>      +            ---\n>      +        ok 5 - strvec::pushf\n>               ok 6 - strvec::pushl\n>               ok 7 - strvec::pushv\n>               ok 8 - strvec::replace_at_head\n>      @@ t/unit-tests/strvec.c (new)\n>       +#define check_strvec(vec, ...) \\\n>       +\tdo { \\\n>       +\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n>      -+\t\tcl_assert(ARRAY_SIZE(expect) > 0); \\\n>      -+\t\tcl_assert_equal_p(expect[ARRAY_SIZE(expect) - 1], NULL); \\\n>      -+\t\tcl_assert_equal_i((vec)->nr, ARRAY_SIZE(expect) - 1); \\\n>      ++\t\tsize_t expect_len = ARRAY_SIZE(expect); \\\n>      ++\t\tcl_assert(expect_len > 0); \\\n>      ++\t\tcl_assert_equal_p(expect[expect_len - 1], NULL); \\\n>      ++\t\tcl_assert_equal_i((vec)->nr, expect_len - 1); \\\n>       +\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n>      -+\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) \\\n>      ++\t\tfor (size_t i = 0; i < expect_len; i++) \\\n>       +\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n>       +\t} while (0)\n>       +\n>       +void test_strvec__init(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tcl_assert_equal_p(vec.v, empty_strvec);\n>       +\tcl_assert_equal_i(vec.nr, 0);\n>       +\tcl_assert_equal_i(vec.alloc, 0);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__dynamic_init(void)\n>       +{\n>       +\tstruct strvec vec;\n>      ++\n>       +\tstrvec_init(&vec);\n>       +\tcl_assert_equal_p(vec.v, empty_strvec);\n>       +\tcl_assert_equal_i(vec.nr, 0);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__clear(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_push(&vec, \"foo\");\n>       +\tstrvec_clear(&vec);\n>       +\tcl_assert_equal_p(vec.v, empty_strvec);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__pushf(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushf(&vec, \"foo: %d\", 1);\n>       +\tcheck_strvec(&vec, \"foo: 1\", NULL);\n>       +\tstrvec_clear(&vec);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__pushl(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tstrvec_clear(&vec);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__replace_at_head(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tstrvec_replace(&vec, 0, \"replaced\");\n>       +\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__replace_in_between(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tstrvec_replace(&vec, 1, \"replaced\");\n>       +\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__replace_with_substring(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushl(&vec, \"foo\", NULL);\n>       +\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n>       +\tcheck_strvec(&vec, \"oo\", NULL);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__remove_at_head(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tstrvec_remove(&vec, 0);\n>       +\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__remove_at_tail(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tstrvec_remove(&vec, 2);\n>       +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__remove_in_between(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tstrvec_remove(&vec, 1);\n>       +\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__pop_empty_array(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pop(&vec);\n>       +\tcheck_strvec(&vec, NULL);\n>       +\tstrvec_clear(&vec);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__pop_non_empty_array(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tstrvec_pop(&vec);\n>       +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__split_empty_string(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_split(&vec, \"\");\n>       +\tcheck_strvec(&vec, NULL);\n>       +\tstrvec_clear(&vec);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__split_single_item(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_split(&vec, \"foo\");\n>       +\tcheck_strvec(&vec, \"foo\", NULL);\n>       +\tstrvec_clear(&vec);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__split_multiple_items(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_split(&vec, \"foo bar baz\");\n>       +\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n>       +\tstrvec_clear(&vec);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__split_whitespace_only(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_split(&vec, \" \\t\\n\");\n>       +\tcheck_strvec(&vec, NULL);\n>       +\tstrvec_clear(&vec);\n>      @@ t/unit-tests/strvec.c (new)\n>       +void test_strvec__split_multiple_consecutive_whitespaces(void)\n>       +{\n>       +\tstruct strvec vec = STRVEC_INIT;\n>      ++\n>       +\tstrvec_split(&vec, \"foo\\n\\t bar\");\n>       +\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n>       +\tstrvec_clear(&vec);\n> 12:  1ac2e48a7f2 ! 13:  c8360db2f86 t/unit-tests: convert ctype tests to use clar\n>      @@ Commit message\n>           on failure:\n>       \n>               # start of suite 1: ctype\n>      -        ok 1 - ctype::isspace\n>      -        not ok 2 - ctype::isdigit\n>      +        not ok 1 - ctype::isspace\n>                   ---\n>                   reason: |\n>                     Test failed.\n>      -              0x61 is classified incorrectly\n>      +              0x0d is classified incorrectly: expected 0, got 1\n>                   at:\n>                     file: 't/unit-tests/ctype.c'\n>      -              line: 38\n>      -              function: 'test_ctype__isdigit'\n>      +              line: 36\n>      +              function: 'test_ctype__isspace'\n>                   ---\n>      +        ok 2 - ctype::isdigit\n>               ok 3 - ctype::isalpha\n>               ok 4 - ctype::isalnum\n>               ok 5 - ctype::is_glob_special\n>      @@ t/unit-tests/t-ctype.c => t/unit-tests/ctype.c\n>       -\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n>       -\t\t} \\\n>       -\t\tcheck(!class(EOF)); \\\n>      --\t} \\\n>      -+\tfor (int i = 0; i < 256; i++) \\\n>      -+\t\tif (class(i) != !!memchr(string, i, len)) \\\n>      -+\t\t\tcl_failf(\"0x%02x is classified incorrectly\", i); \\\n>      ++\tfor (int i = 0; i < 256; i++) { \\\n>      ++\t\tint actual = class(i), expect = !!memchr(string, i, len); \\\n>      ++\t\tif (actual != expect) \\\n>      ++\t\t\tcl_failf(\"0x%02x is classified incorrectly: expected %d, got %d\", \\\n>      ++\t\t\t\t i, expect, actual); \\\n>      + \t} \\\n>       +\tcl_assert(!class(EOF)); \\\n>        } while (0)\n>        \n>      @@ t/unit-tests/unit-test.h\n>       +#include \"strbuf.h\"\n>       +\n>       +#define cl_failf(fmt, ...) do { \\\n>      -+\tchar *desc = xstrfmt(fmt, __VA_ARGS__); \\\n>      ++\tchar desc[4096]; \\\n>      ++\tsnprintf(desc, sizeof(desc), fmt, __VA_ARGS__); \\\n>       +\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n>      -+\tfree(desc); \\\n>       +} while (0)\n> 13:  131036c398e = 14:  d51c146cd9d clar: add CMake support\n"},{"id":"502115","messageId":"ZthqtWVntB-PgSH-@pks.im","threadId":"61874","inReplyTo":"e435e05a-376c-4928-9852-6925df855399@gmail.com","subject":"Re: [PATCH v7 11/14] t/unit-tests: implement test driver","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:12:05Z","receivedAt":"2024-09-04T14:12:11Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 04, 2024 at 02:35:20PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 03/09/2024 10:15, Patrick Steinhardt wrote:\n> > The test driver in \"unit-test.c\" is responsible for setting up our unit\n> > tests and eventually running them. As such, it is also responsible for\n> > parsing the command line arguments.\n> > \n> > The clar unit testing framework provides function `clar_test()` that\n> > parses command line arguments and then executes the tests for us. In\n> > theory that would already be sufficient. We have the special requirement\n> > to always generate TAP-formatted output though, so we'd have to always\n> > pass the \"-t\" argument to clar. Furthermore, some of the options exposed\n> > by clar are ineffective when \"-t\" is used, but they would still be shown\n> > when the user passes the \"-h\" parameter to have the clar show its usage.\n> > \n> > Implement our own option handling instead of using the one provided by\n> > clar, which gives us greater flexibility in how exactly we set things\n> > up.\n> \n> That makes sense\n> \n> > We would ideally not use any \"normal\" code of ours for this such that\n> > the unit testing framework doesn't depend on it working correctly. But\n> > it is somewhat dubious whether we really want to reimplement all of the\n> > option parsing. So for now, let's be pragmatic and reuse it until we\n> > find a good reason in the future why we'd really want to avoid it.\n> \n> I think that's fine for now. Using parse_options() gives a much nicer user\n> experience than clar_test() as it supports long options and has more\n> flexible support for option arguments. I'd expect the code that implements\n> \"struct string_list\" and \"struct strvec\" to be pretty stable so its probably\n> safe to rely on those.\n> \n> Given there's only a couple of options it wouldn't be too bad to implement\n> the parsing ourselves if we have to in the future. We might need to do that\n> to support the libification work as I suspect we wont want to link tests for\n> other libraries against libgit.a.\n> \n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >   t/unit-tests/unit-test.c | 43 ++++++++++++++++++++++++++++++++++++++--\n> >   1 file changed, 41 insertions(+), 2 deletions(-)\n> > \n> > diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\n> > index 3d12cde6dae..96fa64de71d 100644\n> > --- a/t/unit-tests/unit-test.c\n> > +++ b/t/unit-tests/unit-test.c\n> > @@ -1,6 +1,45 @@\n> >   #include \"unit-test.h\"\n> > +#include \"parse-options.h\"\n> > +#include \"string-list.h\"\n> > +#include \"strvec.h\"\n> > -int cmd_main(int argc UNUSED, const char **argv UNUSED)\n> > +static const char * const unit_test_usage[] = {\n> > +\tN_(\"unit-test [<options>]\"),\n> > +\tNULL,\n> > +};\n> > +\n> > +int cmd_main(int argc, const char **argv)\n> >   {\n> > -\treturn 0;\n> > +\tstruct string_list run_args = STRING_LIST_INIT_NODUP;\n> > +\tstruct string_list exclude_args = STRING_LIST_INIT_NODUP;\n> > +\tint immediate = 0;\n> > +\tstruct option options[] = {\n> > +\t\tOPT_BOOL('i', \"--immediate\", &immediate,\n> > +\t\t\t N_(\"immediately exit upon the first failed test\")),\n> \n> This is unused. If we want to to behave like the \"--immediate\" option of our\n> integration tests that's  hard to implement by wrapping clar_test() which\n> requires \"-i<suite>\". The simplest thing would be to just drop it for now.\n> Otherwise as the most likely use for \"-i\" is manually testing some tests in\n> a suite we could require \"--run\" with \"-i\". Then we would have one or more\n> suite names which we can append to \"-i\" when passing it to clar_test().\n> Alternatively we could include \"clar.suite\" and wade through all the test\n> suite names ourselves to construct a suitable list of \"-i\" options to pass\n> to clar_test() but that would probably mean we have to parse the excludes as\n> well which makes it all a bit of a faff.\n\nOh, that's a plain oversight on my side. It is easy to wire up given\nthat the clar already supports it via \"-Q\". Also made me notice that I\nwrote \"--immediate\" instead of \"immediate\".\n\n> > +\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"name\"),\n> > +\t\t\t\tN_(\"run only test suite or individual test <name>\")),\n> \n> It's nice that this option name now matches our integration tests. It would\n> be helpful to show the syntax for \"name\" (I think it expects\n> <suite>[::<test>]) but I failed to come up with a concise description to add\n> to the help here.\n\nIsn't `<suite[::test]>` concise enough? I certainly like it.\n\nPatrick\n"},{"id":"502116","messageId":"Zthq3tGQAe-0N6C4@pks.im","threadId":"61874","inReplyTo":"b177ffce-f5cb-4df9-8b81-79128f9185d0@gmail.com","subject":"Re: [PATCH v7 00/14] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:12:46Z","receivedAt":"2024-09-04T14:12:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 04, 2024 at 02:35:33PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 03/09/2024 10:14, Patrick Steinhardt wrote:\n> > Hi,\n> > \n> > this is the seventh version fo my patch series that introduces the clar\n> > unit testing framework.\n> > \n> > Changes compared to v6:\n> > \n> >    - Add some explanations for why we introduce clar in the first place\n> >      to the second commit message.\n> > \n> >    - Split out implementation of the test driver, that is the main\n> >      function, into a separate commit.\n> > \n> >    - Reimplement argument parsing ourselves. This allows more flexibility\n> >      and gets rid of some options that do not make sense for us.\n> > \n> >    - Add an empty line between declarations and code.\n> > \n> >    - Improve the test messages in ctype tests to also mention our\n> >      expectations.\n> > \n> >    - Adapt `cl_failf()` to not use `xstrfmt()`, but `snprintf()` with a\n> >      static buffer instead.\n> \n> I've left some comments on patch 11 which is new. Apart from that range-diff\n> looks fine to me.\n> \n> Thanks for working on this\n> \n> Phillip\n\nThanks for all your reviews on this series, highly appreciated!\n\nPatrick\n"},{"id":"502117","messageId":"cover.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1722415748.git.ps@pks.im","subject":"[PATCH v8 00/14] Introduce clar testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:16:42Z","receivedAt":"2024-09-04T14:16:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis is the 8th version of my patch series that introduces the clar\ntesting framework.\n\nChanges compared to v7:\n\n  - Properly wire up the \"--immediate\" flag.\n\n  - Give a hint for the syntax to run only specific suites or tests for\n    the \"--run\" and \"--exclude\" options.\n\nThanks!\n\nPatrick\n\nJohannes Schindelin (4):\n  clar: avoid compile error with mingw-w64\n  clar(win32): avoid compile error due to unused `fs_copy()`\n  clar: stop including `shellapi.h` unnecessarily\n  clar: add CMake support\n\nPatrick Steinhardt (10):\n  t: do not pass GIT_TEST_OPTS to unit tests with prove\n  t: import the clar unit testing framework\n  t/clar: fix compatibility with NonStop\n  Makefile: fix sparse dependency on GENERATED_H\n  Makefile: make hdr-check depend on generated headers\n  Makefile: do not use sparse on third-party sources\n  Makefile: wire up the clar unit testing framework\n  t/unit-tests: implement test driver\n  t/unit-tests: convert strvec tests to use clar\n  t/unit-tests: convert ctype tests to use clar\n\n .gitignore                                 |   1 +\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |  53 +-\n contrib/buildsystems/CMakeLists.txt        |  53 ++\n t/Makefile                                 |   4 +-\n t/run-test.sh                              |   2 +-\n t/unit-tests/.gitignore                    |   2 +\n t/unit-tests/clar-generate.awk             |  50 ++\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 524 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n t/unit-tests/strvec.c                      | 241 ++++++\n t/unit-tests/t-strvec.c                    | 211 ------\n t/unit-tests/unit-test.c                   |  47 ++\n t/unit-tests/unit-test.h                   |  10 +\n 31 files changed, 3459 insertions(+), 234 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n rename t/unit-tests/{t-ctype.c => ctype.c} (68%)\n create mode 100644 t/unit-tests/strvec.c\n delete mode 100644 t/unit-tests/t-strvec.c\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\nRange-diff against v7:\n 1:  b67f10ec0b0 =  1:  b67f10ec0b0 t: do not pass GIT_TEST_OPTS to unit tests with prove\n 2:  55a9b46e65f =  2:  55a9b46e65f t: import the clar unit testing framework\n 3:  f24401f0a87 =  3:  f24401f0a87 t/clar: fix compatibility with NonStop\n 4:  658a601c541 =  4:  658a601c541 clar: avoid compile error with mingw-w64\n 5:  0b8a6ac5fed =  5:  0b8a6ac5fed clar(win32): avoid compile error due to unused `fs_copy()`\n 6:  c50e7a0ea68 =  6:  c50e7a0ea68 clar: stop including `shellapi.h` unnecessarily\n 7:  b8f3f16dd27 =  7:  b8f3f16dd27 Makefile: fix sparse dependency on GENERATED_H\n 8:  3d3fe443b9a =  8:  3d3fe443b9a Makefile: make hdr-check depend on generated headers\n 9:  7d0f494850a =  9:  7d0f494850a Makefile: do not use sparse on third-party sources\n10:  9c74c5ae019 = 10:  9c74c5ae019 Makefile: wire up the clar unit testing framework\n11:  8bd5b3e2b29 ! 11:  81d932bfa33 t/unit-tests: implement test driver\n    @@ t/unit-tests/unit-test.c\n     +\tstruct string_list exclude_args = STRING_LIST_INIT_NODUP;\n     +\tint immediate = 0;\n     +\tstruct option options[] = {\n    -+\t\tOPT_BOOL('i', \"--immediate\", &immediate,\n    ++\t\tOPT_BOOL('i', \"immediate\", &immediate,\n     +\t\t\t N_(\"immediately exit upon the first failed test\")),\n    -+\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"name\"),\n    -+\t\t\t\tN_(\"run only test suite or individual test <name>\")),\n    -+\t\tOPT_STRING_LIST('x', \"exclude\", &exclude_args, N_(\"name\"),\n    -+\t\t\t\tN_(\"exclude test suite <name>\")),\n    ++\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"suite[::test]\"),\n    ++\t\t\t\tN_(\"run only test suite or individual test <suite[::test]>\")),\n    ++\t\tOPT_STRING_LIST('x', \"exclude\", &exclude_args, N_(\"suite\"),\n    ++\t\t\t\tN_(\"exclude test suite <suite>\")),\n     +\t\tOPT_END(),\n     +\t};\n     +\tstruct strvec args = STRVEC_INIT;\n    @@ t/unit-tests/unit-test.c\n     +\n     +\tstrvec_push(&args, argv[0]);\n     +\tstrvec_push(&args, \"-t\");\n    ++\tif (immediate)\n    ++\t\tstrvec_push(&args, \"-Q\");\n     +\tfor (size_t i = 0; i < run_args.nr; i++)\n     +\t\tstrvec_pushf(&args, \"-s%s\", run_args.items[i].string);\n     +\tfor (size_t i = 0; i < exclude_args.nr; i++)\n12:  3c3b9eacdfb = 12:  604303e31aa t/unit-tests: convert strvec tests to use clar\n13:  c8360db2f86 = 13:  ba05b9f1eef t/unit-tests: convert ctype tests to use clar\n14:  d51c146cd9d = 14:  8441d29daa8 clar: add CMake support\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502118","messageId":"b67f10ec0b08699eec93e3a7eb6ef3e06b5cedba.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 01/14] t: do not pass GIT_TEST_OPTS to unit tests with prove","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:16:46Z","receivedAt":"2024-09-04T14:16:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When using the prove target, we append GIT_TEST_OPTS to the arguments\nthat we execute each of the tests with. This doesn't only include the\nintended test scripts, but also ends up passing the arguments to our\nunit tests. This is unintentional though as they do not even know to\ninterpret those arguments, and is inconsistent with how we execute unit\ntests without prove.\n\nThis isn't much of an issue because our current set of unit tests mostly\nignore their arguments anyway. With the introduction of clar-based unit\ntests this is about to become an issue though, as these do parse their\ncommand line argument to alter behaviour.\n\nPrepare for this by passing GIT_TEST_OPTS to \"run-test.sh\" via an\nenvironment variable. Like this, we can conditionally forward it to our\ntest scripts, only.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/Makefile    | 3 ++-\n t/run-test.sh | 2 +-\n 2 files changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/Makefile b/t/Makefile\nindex 4c30e7c06fb..d2212de0b78 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -68,7 +68,8 @@ failed:\n \ttest -z \"$$failed\" || $(MAKE) $$failed\n \n prove: pre-clean check-chainlint $(TEST_LINT)\n-\t@echo \"*** prove (shell & unit tests) ***\"; $(CHAINLINTSUPPRESS) TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS) :: $(GIT_TEST_OPTS)\n+\t@echo \"*** prove (shell & unit tests) ***\"\n+\t@$(CHAINLINTSUPPRESS) TEST_OPTIONS='$(GIT_TEST_OPTS)' TEST_SHELL_PATH='$(TEST_SHELL_PATH_SQ)' $(PROVE) --exec ./run-test.sh $(GIT_PROVE_OPTS) $(T) $(UNIT_TESTS)\n \t$(MAKE) clean-except-prove-cache\n \n $(T):\ndiff --git a/t/run-test.sh b/t/run-test.sh\nindex 13c353b91b4..63328ac630c 100755\n--- a/t/run-test.sh\n+++ b/t/run-test.sh\n@@ -10,7 +10,7 @@ case \"$1\" in\n \t\techo >&2 \"ERROR: TEST_SHELL_PATH is empty or not set\"\n \t\texit 1\n \tfi\n-\texec \"${TEST_SHELL_PATH}\" \"$@\"\n+\texec \"${TEST_SHELL_PATH}\" \"$@\" ${TEST_OPTIONS}\n \t;;\n *)\n \texec \"$@\"\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502120","messageId":"55a9b46e65f2072ca7994b042dfe24d1c153c334.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 02/14] t: import the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:16:48Z","receivedAt":"2024-09-04T14:16:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Our unit testing framework is a homegrown solution. While it supports\nmost of our needs, it is likely that the volume of unit tests will grow\nquite a bit in the future such that we can exercise low-level subsystems\ndirectly. This surfaces several shortcomings that the current solution\nhas:\n\n  - There is no way to run only one specific tests. While some of our\n    unit tests wire this up manually, others don't. In general, it\n    requires quite a bit of boilerplate to get this set up correctly.\n\n  - Failures do not cause a test to stop execution directly. Instead,\n    the test author needs to return manually whenever an assertion\n    fails. This is rather verbose and is not done correctly in most of\n    our unit tests.\n\n  - Wiring up a new testcase requires both implementing the test\n    function and calling it in the respective test suite's main\n    function, which is creating code duplication.\n\nWe can of course fix all of these issues ourselves, but that feels\nrather pointless when there are already so many unit testing frameworks\nout there that have those features.\n\nWe line out some requirements for any unit testing framework in\n\"Documentation/technical/unit-tests.txt\". The \"clar\" unit testing\nframework, which isn't listed in that table yet, ticks many of the\nboxes:\n\n  - It is licensed under ISC, which is compatible.\n\n  - It is easily vendorable because it is rather tiny at around 1200\n    lines of code.\n\n  - It is easily hackable due to the same reason.\n\n  - It has TAP support.\n\n  - It has skippable tests.\n\n  - It preprocesses test files in order to extract test functions, which\n    then get wired up automatically.\n\nWhile it's not perfect, the fact that clar originates from the libgit2\nproject means that it should be rather easy for us to collaborate with\nupstream to plug any gaps.\n\nImport the clar unit testing framework at commit 1516124 (Merge pull\nrequest #97 from pks-t/pks-whitespace-fixes, 2024-08-15). The framework\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/technical/unit-tests.txt     |   2 +\n Makefile                                   |   4 +-\n t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n t/unit-tests/clar/COPYING                  |  15 +\n t/unit-tests/clar/README.md                | 329 ++++++++\n t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n t/unit-tests/clar/clar.h                   | 173 +++++\n t/unit-tests/clar/clar/fixtures.h          |  50 ++\n t/unit-tests/clar/clar/fs.h                | 522 +++++++++++++\n t/unit-tests/clar/clar/print.h             | 211 ++++++\n t/unit-tests/clar/clar/sandbox.h           | 153 ++++\n t/unit-tests/clar/clar/summary.h           | 143 ++++\n t/unit-tests/clar/generate.py              | 266 +++++++\n t/unit-tests/clar/test/.gitignore          |   4 +\n t/unit-tests/clar/test/Makefile            |  39 +\n t/unit-tests/clar/test/clar_test.h         |  16 +\n t/unit-tests/clar/test/main.c              |  40 +\n t/unit-tests/clar/test/main.c.sample       |  27 +\n t/unit-tests/clar/test/resources/test/file |   1 +\n t/unit-tests/clar/test/sample.c            |  84 ++\n 20 files changed, 2943 insertions(+), 1 deletion(-)\n create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n create mode 100644 t/unit-tests/clar/COPYING\n create mode 100644 t/unit-tests/clar/README.md\n create mode 100644 t/unit-tests/clar/clar.c\n create mode 100644 t/unit-tests/clar/clar.h\n create mode 100644 t/unit-tests/clar/clar/fixtures.h\n create mode 100644 t/unit-tests/clar/clar/fs.h\n create mode 100644 t/unit-tests/clar/clar/print.h\n create mode 100644 t/unit-tests/clar/clar/sandbox.h\n create mode 100644 t/unit-tests/clar/clar/summary.h\n create mode 100755 t/unit-tests/clar/generate.py\n create mode 100644 t/unit-tests/clar/test/.gitignore\n create mode 100644 t/unit-tests/clar/test/Makefile\n create mode 100644 t/unit-tests/clar/test/clar_test.h\n create mode 100644 t/unit-tests/clar/test/main.c\n create mode 100644 t/unit-tests/clar/test/main.c.sample\n create mode 100644 t/unit-tests/clar/test/resources/test/file\n create mode 100644 t/unit-tests/clar/test/sample.c\n\ndiff --git a/Documentation/technical/unit-tests.txt b/Documentation/technical/unit-tests.txt\nindex 206037ffb19..5a432b7b29c 100644\n--- a/Documentation/technical/unit-tests.txt\n+++ b/Documentation/technical/unit-tests.txt\n@@ -203,6 +203,7 @@ GitHub / GitLab stars to estimate this.\n :criterion: https://github.com/Snaipe/Criterion[Criterion]\n :c-tap: https://github.com/rra/c-tap-harness/[C TAP]\n :check: https://libcheck.github.io/check/[Check]\n+:clar: https://github.com/clar-test/clar[Clar]\n \n [format=\"csv\",options=\"header\",width=\"33%\",subs=\"specialcharacters,attributes,quotes,macros\"]\n |=====\n@@ -212,6 +213,7 @@ Framework,\"<<license,License>>\",\"<<vendorable-or-ubiquitous,Vendorable or ubiqui\n {criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800\n {c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33\n {check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973\n+{clar},{isc},{false},{partial},{true},{true},{true},{true},{false},{false},{true},1,192\n |=====\n \n === Additional framework candidates\ndiff --git a/Makefile b/Makefile\nindex a87e18b317d..28742a60964 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1331,6 +1331,8 @@ THIRD_PARTY_SOURCES += compat/poll/%\n THIRD_PARTY_SOURCES += compat/regex/%\n THIRD_PARTY_SOURCES += sha1collisiondetection/%\n THIRD_PARTY_SOURCES += sha1dc/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n+THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n@@ -3261,7 +3263,7 @@ $(SP_OBJ): %.sp: %.c %.o\n .PHONY: sparse\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/% $(UNIT_TEST_DIR)/clar/% $(UNIT_TEST_DIR)/clar/clar/%\n ifndef OPENSSL_SHA1\n \tEXCEPT_HDRS += sha1/openssl.h\n endif\ndiff --git a/t/unit-tests/clar/.github/workflows/ci.yml b/t/unit-tests/clar/.github/workflows/ci.yml\nnew file mode 100644\nindex 00000000000..b1ac2de460a\n--- /dev/null\n+++ b/t/unit-tests/clar/.github/workflows/ci.yml\n@@ -0,0 +1,23 @@\n+name: CI\n+\n+on:\n+  push:\n+    branches: [ main ]\n+  pull_request:\n+    branches: [ main ]\n+\n+jobs:\n+  build:\n+    strategy:\n+      matrix:\n+        os: [ ubuntu-latest, macos-latest ]\n+\n+    runs-on: ${{ matrix.os }}\n+\n+    steps:\n+    - name: Check out\n+      uses: actions/checkout@v2\n+    - name: Build\n+      run: |\n+        cd test\n+        make\ndiff --git a/t/unit-tests/clar/COPYING b/t/unit-tests/clar/COPYING\nnew file mode 100644\nindex 00000000000..8983817f0c9\n--- /dev/null\n+++ b/t/unit-tests/clar/COPYING\n@@ -0,0 +1,15 @@\n+ISC License\n+\n+Copyright (c) 2011-2015 Vicent Marti\n+\n+Permission to use, copy, modify, and/or distribute this software for any\n+purpose with or without fee is hereby granted, provided that the above\n+copyright notice and this permission notice appear in all copies.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ndiff --git a/t/unit-tests/clar/README.md b/t/unit-tests/clar/README.md\nnew file mode 100644\nindex 00000000000..a8961c5f10f\n--- /dev/null\n+++ b/t/unit-tests/clar/README.md\n@@ -0,0 +1,329 @@\n+Come out and Clar\n+=================\n+\n+In Catalan, \"clar\" means clear, easy to perceive.  Using clar will make it\n+easy to test and make clear the quality of your code.\n+\n+> _Historical note_\n+>\n+> Originally the clar project was named \"clay\" because the word \"test\" has its\n+> roots in the latin word *\"testum\"*, meaning \"earthen pot\", and *\"testa\"*,\n+> meaning \"piece of burned clay\"?\n+>\n+> This is because historically, testing implied melting metal in a pot to\n+> check its quality.  Clay is what tests are made of.\n+\n+## Quick Usage Overview\n+\n+Clar is a minimal C unit testing framework. It's been written to replace the\n+old framework in [libgit2][libgit2], but it's both very versatile and\n+straightforward to use.\n+\n+Can you count to funk?\n+\n+- **Zero: Initialize test directory**\n+\n+    ~~~~ sh\n+    $ mkdir tests\n+    $ cp -r $CLAR_ROOT/clar* tests\n+    $ cp $CLAR_ROOT/test/clar_test.h tests\n+    $ cp $CLAR_ROOT/test/main.c.sample tests/main.c\n+    ~~~~\n+\n+- **One: Write some tests**\n+\n+    File: tests/adding.c:\n+\n+    ~~~~ c\n+    /* adding.c for the \"Adding\" suite */\n+    #include \"clar.h\"\n+\n+    static int *answer;\n+\n+    void test_adding__initialize(void)\n+    {\n+        answer = malloc(sizeof(int));\n+        cl_assert_(answer != NULL, \"No memory left?\");\n+        *answer = 42;\n+    }\n+\n+    void test_adding__cleanup(void)\n+    {\n+        free(answer);\n+    }\n+\n+    void test_adding__make_sure_math_still_works(void)\n+    {\n+        cl_assert_(5 > 3, \"Five should probably be greater than three\");\n+        cl_assert_(-5 < 2, \"Negative numbers are small, I think\");\n+        cl_assert_(*answer == 42, \"The universe is doing OK. And the initializer too.\");\n+    }\n+    ~~~~~\n+\n+- **Two: Build the test executable**\n+\n+    ~~~~ sh\n+    $ cd tests\n+    $ $CLAR_PATH/generate.py .\n+    Written `clar.suite` (1 suites)\n+    $ gcc -I. clar.c main.c adding.c -o testit\n+    ~~~~\n+\n+- **Funk: Funk it.**\n+\n+    ~~~~ sh\n+    $ ./testit\n+    ~~~~\n+\n+## The Clar Test Suite\n+\n+Writing a test suite is pretty straightforward. Each test suite is a `*.c`\n+file with a descriptive name: this encourages modularity.\n+\n+Each test suite has optional initialize and cleanup methods. These methods\n+will be called before and after running **each** test in the suite, even if\n+such test fails. As a rule of thumb, if a test needs a different initializer\n+or cleanup method than another test in the same module, that means it\n+doesn't belong in that module. Keep that in mind when grouping tests\n+together.\n+\n+The `initialize` and `cleanup` methods have the following syntax, with\n+`suitename` being the current suite name, e.g. `adding` for the `adding.c`\n+suite.\n+\n+~~~~ c\n+void test_suitename__initialize(void)\n+{\n+    /* init */\n+}\n+\n+void test_suitename__cleanup(void)\n+{\n+    /* cleanup */\n+}\n+~~~~\n+\n+These methods are encouraged to use static, global variables to store the state\n+that will be used by all tests inside the suite.\n+\n+~~~~ c\n+static git_repository *_repository;\n+\n+void test_status__initialize(void)\n+{\n+    create_tmp_repo(STATUS_REPO);\n+    git_repository_open(_repository, STATUS_REPO);\n+}\n+\n+void test_status__cleanup(void)\n+{\n+    git_repository_close(_repository);\n+    git_path_rm(STATUS_REPO);\n+}\n+\n+void test_status__simple_test(void)\n+{\n+    /* do something with _repository */\n+}\n+~~~~\n+\n+Writing the actual tests is just as straightforward. Tests have the\n+`void test_suitename__test_name(void)` signature, and they should **not**\n+be static. Clar will automatically detect and list them.\n+\n+Tests are run as they appear on their original suites: they have no return\n+value. A test is considered \"passed\" if it doesn't raise any errors. Check\n+the \"Clar API\" section to see the various helper functions to check and\n+raise errors during test execution.\n+\n+__Caution:__ If you use assertions inside of `test_suitename__initialize`,\n+make sure that you do not rely on `__initialize` being completely run\n+inside your `test_suitename__cleanup` function. Otherwise you might\n+encounter ressource cleanup twice.\n+\n+## How does Clar work?\n+\n+To use Clar:\n+\n+1. copy the Clar boilerplate to your test directory\n+2. copy (and probably modify) the sample `main.c` (from\n+   `$CLAR_PATH/test/main.c.sample`)\n+3. run the Clar mixer (a.k.a. `generate.py`) to scan your test directory and\n+   write out the test suite metadata.\n+4. compile your test files and the Clar boilerplate into a single test\n+   executable\n+5. run the executable to test!\n+\n+The Clar boilerplate gives you a set of useful test assertions and features\n+(like accessing or making sandbox copies of fixture data).  It consists of\n+the `clar.c` and `clar.h` files, plus the code in the `clar/` subdirectory.\n+You should not need to edit these files.\n+\n+The sample `main.c` (i.e. `$CLAR_PATH/test/main.c.sample`) file invokes\n+`clar_test(argc, argv)` to run the tests.  Usually, you will edit this file\n+to perform any framework specific initialization and teardown that you need.\n+\n+The Clar mixer (`generate.py`) recursively scans your test directory for\n+any `.c` files, parses them, and writes the `clar.suite` file with all of\n+the metadata about your tests.  When you build, the `clar.suite` file is\n+included into `clar.c`.\n+\n+The mixer can be run with **Python 2.5, 2.6, 2.7, 3.0, 3.1, 3.2 and PyPy 1.6**.\n+\n+Commandline usage of the mixer is as follows:\n+\n+    $ ./generate.py .\n+\n+Where `.` is the folder where all the test suites can be found. The mixer\n+will automatically locate all the relevant source files and build the\n+testing metadata. The metadata will be written to `clar.suite`, in the same\n+folder as all the test suites. This file is included by `clar.c` and so\n+must be accessible via `#include` when building the test executable.\n+\n+    $ gcc -I. clar.c main.c suite1.c test2.c -o run_tests\n+\n+**Note that the Clar mixer only needs to be ran when adding new tests to a\n+suite, in order to regenerate the metadata**. As a result, the `clar.suite`\n+file can be checked into version control if you wish to be able to build\n+your test suite without having to re-run the mixer.\n+\n+This is handy when e.g. generating tests in a local computer, and then\n+building and testing them on an embedded device or a platform where Python\n+is not available.\n+\n+### Fixtures\n+\n+Clar can create sandboxed fixtures for you to use in your test. You'll need to compile *clar.c* with an additional `CFLAG`, `-DCLAR_FIXTURE_PATH`. This should be an absolute path to your fixtures directory.\n+\n+Once that's done, you can use the fixture API as defined below.\n+\n+## The Clar API\n+\n+Clar makes the following methods available from all functions in a test\n+suite.\n+\n+-   `cl_must_pass(call)`, `cl_must_pass_(call, message)`: Verify that the given\n+    function call passes, in the POSIX sense (returns a value greater or equal\n+    to 0).\n+\n+-   `cl_must_fail(call)`, `cl_must_fail_(call, message)`: Verify that the given\n+    function call fails, in the POSIX sense (returns a value less than 0).\n+\n+-   `cl_assert(expr)`, `cl_assert_(expr, message)`: Verify that `expr` is true.\n+\n+-   `cl_check_pass(call)`, `cl_check_pass_(call, message)`: Verify that the\n+    given function call passes, in the POSIX sense (returns a value greater or\n+    equal to 0). If the function call doesn't succeed, a test failure will be\n+    logged but the test's execution will continue.\n+\n+-   `cl_check_fail(call)`, `cl_check_fail_(call, message)`: Verify that the\n+    given function call fails, in the POSIX sense (returns a value less than\n+    0). If the function call doesn't fail, a test failure will be logged but\n+    the test's execution will continue.\n+\n+-   `cl_check(expr)`: Verify that `expr` is true. If `expr` is not\n+    true, a test failure will be logged but the test's execution will continue.\n+\n+-   `cl_fail(message)`: Fail the current test with the given message.\n+\n+-   `cl_warning(message)`: Issue a warning. This warning will be\n+    logged as a test failure but the test's execution will continue.\n+\n+-   `cl_set_cleanup(void (*cleanup)(void *), void *opaque)`: Set the cleanup\n+    method for a single test. This method will be called with `opaque` as its\n+    argument before the test returns (even if the test has failed).\n+    If a global cleanup method is also available, the local cleanup will be\n+    called first, and then the global.\n+\n+-   `cl_assert_equal_i(int,int)`: Verify that two integer values are equal.\n+    The advantage of this over a simple `cl_assert` is that it will format\n+    a much nicer error report if the values are not equal.\n+\n+-   `cl_assert_equal_s(const char *,const char *)`: Verify that two strings\n+    are equal.  The expected value can also be NULL and this will correctly\n+    test for that.\n+\n+-   `cl_fixture_sandbox(const char *)`: Sets up a sandbox for a fixture\n+    so that you can mutate the file directly.\n+\n+-   `cl_fixture_cleanup(const char *)`: Tears down the previous fixture\n+    sandbox.\n+\n+-   `cl_fixture(const char *)`: Gets the full path to a fixture file.\n+\n+Please do note that these methods are *always* available whilst running a\n+test, even when calling auxiliary/static functions inside the same file.\n+\n+It's strongly encouraged to perform test assertions in auxiliary methods,\n+instead of returning error values. This is considered good Clar style.\n+\n+Style Example:\n+\n+~~~~ c\n+/*\n+ * Bad style: auxiliary functions return an error code\n+ */\n+\n+static int check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    if (aux == NULL)\n+        return -1;\n+\n+    return strcmp(my_function(aux), str) == 0 ? 0 : -1;\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    cl_must_pass_(\n+        check_string(\"foo\"),\n+        \"String differs after processing\"\n+    );\n+\n+    cl_must_pass_(\n+        check_string(\"bar\"),\n+        \"String differs after processing\"\n+    );\n+}\n+~~~~\n+\n+~~~~ c\n+/*\n+ * Good style: auxiliary functions perform assertions\n+ */\n+\n+static void check_string(const char *str)\n+{\n+    const char *aux = process_string(str);\n+\n+    cl_assert_(\n+        aux != NULL,\n+        \"String processing failed\"\n+    );\n+\n+    cl_assert_(\n+        strcmp(my_function(aux), str) == 0,\n+        \"String differs after processing\"\n+    );\n+}\n+\n+void test_example__a_test_with_auxiliary_methods(void)\n+{\n+    check_string(\"foo\");\n+    check_string(\"bar\");\n+}\n+~~~~\n+\n+About Clar\n+==========\n+\n+Clar has been written from scratch by [Vicent Martí](https://github.com/vmg),\n+to replace the old testing framework in [libgit2][libgit2].\n+\n+Do you know what languages are *in* on the SF startup scene? Node.js *and*\n+Latin.  Follow [@vmg](https://www.twitter.com/vmg) on Twitter to\n+receive more lessons on word etymology. You can be hip too.\n+\n+\n+[libgit2]: https://github.com/libgit2/libgit2\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nnew file mode 100644\nindex 00000000000..3fc2c768158\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.c\n@@ -0,0 +1,842 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#include <assert.h>\n+#include <setjmp.h>\n+#include <stdlib.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <math.h>\n+#include <stdarg.h>\n+#include <wchar.h>\n+#include <time.h>\n+\n+/* required for sandboxing */\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#ifdef _WIN32\n+#\tinclude <windows.h>\n+#\tinclude <io.h>\n+#\tinclude <shellapi.h>\n+#\tinclude <direct.h>\n+\n+#\tdefine _MAIN_CC __cdecl\n+\n+#\tifndef stat\n+#\t\tdefine stat(path, st) _stat(path, st)\n+#\tendif\n+#\tifndef mkdir\n+#\t\tdefine mkdir(path, mode) _mkdir(path)\n+#\tendif\n+#\tifndef chdir\n+#\t\tdefine chdir(path) _chdir(path)\n+#\tendif\n+#\tifndef access\n+#\t\tdefine access(path, mode) _access(path, mode)\n+#\tendif\n+#\tifndef strdup\n+#\t\tdefine strdup(str) _strdup(str)\n+#\tendif\n+#\tifndef strcasecmp\n+#\t\tdefine strcasecmp(a,b) _stricmp(a,b)\n+#\tendif\n+\n+#\tifndef __MINGW32__\n+#\t\tpragma comment(lib, \"shell32\")\n+#\t\tifndef strncpy\n+#\t\t\tdefine strncpy(to, from, to_size) strncpy_s(to, to_size, from, _TRUNCATE)\n+#\t\tendif\n+#\t\tifndef W_OK\n+#\t\t\tdefine W_OK 02\n+#\t\tendif\n+#\t\tifndef S_ISDIR\n+#\t\t\tdefine S_ISDIR(x) ((x & _S_IFDIR) != 0)\n+#\t\tendif\n+#\t\tdefine p_snprintf(buf,sz,fmt,...) _snprintf_s(buf,sz,_TRUNCATE,fmt,__VA_ARGS__)\n+#\telse\n+#\t\tdefine p_snprintf snprintf\n+#\tendif\n+\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"Iu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"Ix\"\n+#\tendif\n+\n+#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+\ttypedef struct stat STAT_T;\n+#\telse\n+\ttypedef struct _stat STAT_T;\n+#\tendif\n+#else\n+#\tinclude <sys/wait.h> /* waitpid(2) */\n+#\tinclude <unistd.h>\n+#\tdefine _MAIN_CC\n+#\tdefine p_snprintf snprintf\n+#\tifndef PRIuZ\n+#\t\tdefine PRIuZ \"zu\"\n+#\tendif\n+#\tifndef PRIxZ\n+#\t\tdefine PRIxZ \"zx\"\n+#\tendif\n+\ttypedef struct stat STAT_T;\n+#endif\n+\n+#define MAX(x, y) (((x) > (y)) ? (x) : (y))\n+\n+#include \"clar.h\"\n+\n+static void fs_rm(const char *_source);\n+static void fs_copy(const char *_source, const char *dest);\n+\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name);\n+#endif\n+\n+struct clar_error {\n+\tconst char *file;\n+\tconst char *function;\n+\tsize_t line_number;\n+\tconst char *error_msg;\n+\tchar *description;\n+\n+\tstruct clar_error *next;\n+};\n+\n+struct clar_explicit {\n+\tsize_t suite_idx;\n+\tconst char *filter;\n+\n+\tstruct clar_explicit *next;\n+};\n+\n+struct clar_report {\n+\tconst char *test;\n+\tint test_number;\n+\tconst char *suite;\n+\n+\tenum cl_test_status status;\n+\ttime_t start;\n+\tdouble elapsed;\n+\n+\tstruct clar_error *errors;\n+\tstruct clar_error *last_error;\n+\n+\tstruct clar_report *next;\n+};\n+\n+struct clar_summary {\n+\tconst char *filename;\n+\tFILE *fp;\n+};\n+\n+static struct {\n+\tenum cl_test_status test_status;\n+\n+\tconst char *active_test;\n+\tconst char *active_suite;\n+\n+\tint total_skipped;\n+\tint total_errors;\n+\n+\tint tests_ran;\n+\tint suites_ran;\n+\n+\tenum cl_output_format output_format;\n+\n+\tint report_errors_only;\n+\tint exit_on_error;\n+\tint verbosity;\n+\n+\tint write_summary;\n+\tchar *summary_filename;\n+\tstruct clar_summary *summary;\n+\n+\tstruct clar_explicit *explicit;\n+\tstruct clar_explicit *last_explicit;\n+\n+\tstruct clar_report *reports;\n+\tstruct clar_report *last_report;\n+\n+\tvoid (*local_cleanup)(void *);\n+\tvoid *local_cleanup_payload;\n+\n+\tjmp_buf trampoline;\n+\tint trampoline_enabled;\n+\n+\tcl_trace_cb *pfn_trace_cb;\n+\tvoid *trace_payload;\n+\n+} _clar;\n+\n+struct clar_func {\n+\tconst char *name;\n+\tvoid (*ptr)(void);\n+};\n+\n+struct clar_suite {\n+\tconst char *name;\n+\tstruct clar_func initialize;\n+\tstruct clar_func cleanup;\n+\tconst struct clar_func *tests;\n+\tsize_t test_count;\n+\tint enabled;\n+};\n+\n+/* From clar_print_*.c */\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names);\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count);\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error);\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status failed);\n+static void clar_print_onsuite(const char *suite_name, int suite_index);\n+static void clar_print_onabort(const char *msg, ...);\n+\n+/* From clar_sandbox.c */\n+static void clar_unsandbox(void);\n+static int clar_sandbox(void);\n+\n+/* From summary.h */\n+static struct clar_summary *clar_summary_init(const char *filename);\n+static int clar_summary_shutdown(struct clar_summary *fp);\n+\n+/* Load the declarations for the test suite */\n+#include \"clar.suite\"\n+\n+\n+#define CL_TRACE(ev)\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\tdo {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\tif (_clar.pfn_trace_cb)\t\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t_clar.pfn_trace_cb(ev,\t\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_suite,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.active_test,\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t   _clar.trace_payload);\t\t\t\t\t\\\n+\t} while (0)\n+\n+void cl_trace_register(cl_trace_cb *cb, void *payload)\n+{\n+\t_clar.pfn_trace_cb = cb;\n+\t_clar.trace_payload = payload;\n+}\n+\n+\n+/* Core test functions */\n+static void\n+clar_report_errors(struct clar_report *report)\n+{\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (error = report->errors; error; error = error->next)\n+\t\tclar_print_error(i++, _clar.last_report, error);\n+}\n+\n+static void\n+clar_report_all(void)\n+{\n+\tstruct clar_report *report;\n+\tstruct clar_error *error;\n+\tint i = 1;\n+\n+\tfor (report = _clar.reports; report; report = report->next) {\n+\t\tif (report->status != CL_TEST_FAILURE)\n+\t\t\tcontinue;\n+\n+\t\tfor (error = report->errors; error; error = error->next)\n+\t\t\tclar_print_error(i++, report, error);\n+\t}\n+}\n+\n+#ifdef WIN32\n+# define clar_time DWORD\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\t*out = GetTickCount();\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)*end - (double)*start) / 1000;\n+}\n+#else\n+# include <sys/time.h>\n+\n+# define clar_time struct timeval\n+\n+static void clar_time_now(clar_time *out)\n+{\n+\tstruct timezone tz;\n+\n+\tgettimeofday(out, &tz);\n+}\n+\n+static double clar_time_diff(clar_time *start, clar_time *end)\n+{\n+\treturn ((double)end->tv_sec + (double)end->tv_usec / 1.0E6) -\n+\t       ((double)start->tv_sec + (double)start->tv_usec / 1.0E6);\n+}\n+#endif\n+\n+static void\n+clar_run_test(\n+\tconst struct clar_suite *suite,\n+\tconst struct clar_func *test,\n+\tconst struct clar_func *initialize,\n+\tconst struct clar_func *cleanup)\n+{\n+\tclar_time start, end;\n+\n+\t_clar.trampoline_enabled = 1;\n+\n+\tCL_TRACE(CL_TRACE__TEST__BEGIN);\n+\n+\t_clar.last_report->start = time(NULL);\n+\tclar_time_now(&start);\n+\n+\tif (setjmp(_clar.trampoline) == 0) {\n+\t\tif (initialize->ptr != NULL)\n+\t\t\tinitialize->ptr();\n+\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_BEGIN);\n+\t\ttest->ptr();\n+\t\tCL_TRACE(CL_TRACE__TEST__RUN_END);\n+\t}\n+\n+\tclar_time_now(&end);\n+\n+\t_clar.trampoline_enabled = 0;\n+\n+\tif (_clar.last_report->status == CL_TEST_NOTRUN)\n+\t\t_clar.last_report->status = CL_TEST_OK;\n+\n+\t_clar.last_report->elapsed = clar_time_diff(&start, &end);\n+\n+\tif (_clar.local_cleanup != NULL)\n+\t\t_clar.local_cleanup(_clar.local_cleanup_payload);\n+\n+\tif (cleanup->ptr != NULL)\n+\t\tcleanup->ptr();\n+\n+\tCL_TRACE(CL_TRACE__TEST__END);\n+\n+\t_clar.tests_ran++;\n+\n+\t/* remove any local-set cleanup methods */\n+\t_clar.local_cleanup = NULL;\n+\t_clar.local_cleanup_payload = NULL;\n+\n+\tif (_clar.report_errors_only) {\n+\t\tclar_report_errors(_clar.last_report);\n+\t} else {\n+\t\tclar_print_ontest(suite->name, test->name, _clar.tests_ran, _clar.last_report->status);\n+\t}\n+}\n+\n+static void\n+clar_run_suite(const struct clar_suite *suite, const char *filter)\n+{\n+\tconst struct clar_func *test = suite->tests;\n+\tsize_t i, matchlen;\n+\tstruct clar_report *report;\n+\tint exact = 0;\n+\n+\tif (!suite->enabled)\n+\t\treturn;\n+\n+\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\treturn;\n+\n+\tif (!_clar.report_errors_only)\n+\t\tclar_print_onsuite(suite->name, ++_clar.suites_ran);\n+\n+\t_clar.active_suite = suite->name;\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_BEGIN);\n+\n+\tif (filter) {\n+\t\tsize_t suitelen = strlen(suite->name);\n+\t\tmatchlen = strlen(filter);\n+\t\tif (matchlen <= suitelen) {\n+\t\t\tfilter = NULL;\n+\t\t} else {\n+\t\t\tfilter += suitelen;\n+\t\t\twhile (*filter == ':')\n+\t\t\t\t++filter;\n+\t\t\tmatchlen = strlen(filter);\n+\n+\t\t\tif (matchlen && filter[matchlen - 1] == '$') {\n+\t\t\t\texact = 1;\n+\t\t\t\tmatchlen--;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (i = 0; i < suite->test_count; ++i) {\n+\t\tif (filter && strncmp(test[i].name, filter, matchlen))\n+\t\t\tcontinue;\n+\n+\t\tif (exact && strlen(test[i].name) != matchlen)\n+\t\t\tcontinue;\n+\n+\t\t_clar.active_test = test[i].name;\n+\n+\t\treport = calloc(1, sizeof(struct clar_report));\n+\t\treport->suite = _clar.active_suite;\n+\t\treport->test = _clar.active_test;\n+\t\treport->test_number = _clar.tests_ran;\n+\t\treport->status = CL_TEST_NOTRUN;\n+\n+\t\tif (_clar.reports == NULL)\n+\t\t\t_clar.reports = report;\n+\n+\t\tif (_clar.last_report != NULL)\n+\t\t\t_clar.last_report->next = report;\n+\n+\t\t_clar.last_report = report;\n+\n+\t\tclar_run_test(suite, &test[i], &suite->initialize, &suite->cleanup);\n+\n+\t\tif (_clar.exit_on_error && _clar.total_errors)\n+\t\t\treturn;\n+\t}\n+\n+\t_clar.active_test = NULL;\n+\tCL_TRACE(CL_TRACE__SUITE_END);\n+}\n+\n+static void\n+clar_usage(const char *arg)\n+{\n+\tprintf(\"Usage: %s [options]\\n\\n\", arg);\n+\tprintf(\"Options:\\n\");\n+\tprintf(\"  -sname        Run only the suite with `name` (can go to individual test name)\\n\");\n+\tprintf(\"  -iname        Include the suite with `name`\\n\");\n+\tprintf(\"  -xname        Exclude the suite with `name`\\n\");\n+\tprintf(\"  -v            Increase verbosity (show suite names)\\n\");\n+\tprintf(\"  -q            Only report tests that had an error\\n\");\n+\tprintf(\"  -Q            Quit as soon as a test fails\\n\");\n+\tprintf(\"  -t            Display results in tap format\\n\");\n+\tprintf(\"  -l            Print suite names\\n\");\n+\tprintf(\"  -r[filename]  Write summary file (to the optional filename)\\n\");\n+\texit(-1);\n+}\n+\n+static void\n+clar_parse_args(int argc, char **argv)\n+{\n+\tint i;\n+\n+\t/* Verify options before execute */\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tif (argument[0] != '-' || argument[1] == '\\0'\n+\t\t    || strchr(\"sixvqQtlr\", argument[1]) == NULL) {\n+\t\t\tclar_usage(argv[0]);\n+\t\t}\n+\t}\n+\n+\tfor (i = 1; i < argc; ++i) {\n+\t\tchar *argument = argv[i];\n+\n+\t\tswitch (argument[1]) {\n+\t\tcase 's':\n+\t\tcase 'i':\n+\t\tcase 'x': { /* given suite name */\n+\t\t\tint offset = (argument[2] == '=') ? 3 : 2, found = 0;\n+\t\t\tchar action = argument[1];\n+\t\t\tsize_t j, arglen, suitelen, cmplen;\n+\n+\t\t\targument += offset;\n+\t\t\targlen = strlen(argument);\n+\n+\t\t\tif (arglen == 0)\n+\t\t\t\tclar_usage(argv[0]);\n+\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j) {\n+\t\t\t\tsuitelen = strlen(_clar_suites[j].name);\n+\t\t\t\tcmplen = (arglen < suitelen) ? arglen : suitelen;\n+\n+\t\t\t\tif (strncmp(argument, _clar_suites[j].name, cmplen) == 0) {\n+\t\t\t\t\tint exact = (arglen >= suitelen);\n+\n+\t\t\t\t\t/* Do we have a real suite prefix separated by a\n+\t\t\t\t\t * trailing '::' or just a matching substring? */\n+\t\t\t\t\tif (arglen > suitelen && (argument[suitelen] != ':'\n+\t\t\t\t\t\t    || argument[suitelen + 1] != ':'))\n+\t\t\t\t\t    continue;\n+\n+\t\t\t\t\t++found;\n+\n+\t\t\t\t\tif (!exact)\n+\t\t\t\t\t\t_clar.verbosity = MAX(_clar.verbosity, 1);\n+\n+\t\t\t\t\tswitch (action) {\n+\t\t\t\t\tcase 's': {\n+\t\t\t\t\t\tstruct clar_explicit *explicit =\n+\t\t\t\t\t\t\tcalloc(1, sizeof(struct clar_explicit));\n+\t\t\t\t\t\tassert(explicit);\n+\n+\t\t\t\t\t\texplicit->suite_idx = j;\n+\t\t\t\t\t\texplicit->filter = argument;\n+\n+\t\t\t\t\t\tif (_clar.explicit == NULL)\n+\t\t\t\t\t\t\t_clar.explicit = explicit;\n+\n+\t\t\t\t\t\tif (_clar.last_explicit != NULL)\n+\t\t\t\t\t\t\t_clar.last_explicit->next = explicit;\n+\n+\t\t\t\t\t\t_clar_suites[j].enabled = 1;\n+\t\t\t\t\t\t_clar.last_explicit = explicit;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tcase 'i': _clar_suites[j].enabled = 1; break;\n+\t\t\t\t\tcase 'x': _clar_suites[j].enabled = 0; break;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif (exact)\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tif (!found) {\n+\t\t\t\tclar_print_onabort(\"No suite matching '%s' found.\\n\", argument);\n+\t\t\t\texit(-1);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase 'q':\n+\t\t\t_clar.report_errors_only = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 'Q':\n+\t\t\t_clar.exit_on_error = 1;\n+\t\t\tbreak;\n+\n+\t\tcase 't':\n+\t\t\t_clar.output_format = CL_OUTPUT_TAP;\n+\t\t\tbreak;\n+\n+\t\tcase 'l': {\n+\t\t\tsize_t j;\n+\t\t\tprintf(\"Test suites (use -s<name> to run just one):\\n\");\n+\t\t\tfor (j = 0; j < _clar_suite_count; ++j)\n+\t\t\t\tprintf(\" %3d: %s\\n\", (int)j, _clar_suites[j].name);\n+\n+\t\t\texit(0);\n+\t\t}\n+\n+\t\tcase 'v':\n+\t\t\t_clar.verbosity++;\n+\t\t\tbreak;\n+\n+\t\tcase 'r':\n+\t\t\t_clar.write_summary = 1;\n+\t\t\tfree(_clar.summary_filename);\n+\t\t\t_clar.summary_filename = *(argument + 2) ? strdup(argument + 2) : NULL;\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\tassert(!\"Unexpected commandline argument!\");\n+\t\t}\n+\t}\n+}\n+\n+void\n+clar_test_init(int argc, char **argv)\n+{\n+\tconst char *summary_env;\n+\n+\tif (argc > 1)\n+\t\tclar_parse_args(argc, argv);\n+\n+\tclar_print_init(\n+\t\t(int)_clar_callback_count,\n+\t\t(int)_clar_suite_count,\n+\t\t\"\"\n+\t);\n+\n+\tif (!_clar.summary_filename &&\n+\t    (summary_env = getenv(\"CLAR_SUMMARY\")) != NULL) {\n+\t\t_clar.write_summary = 1;\n+\t\t_clar.summary_filename = strdup(summary_env);\n+\t}\n+\n+\tif (_clar.write_summary && !_clar.summary_filename)\n+\t\t_clar.summary_filename = strdup(\"summary.xml\");\n+\n+\tif (_clar.write_summary &&\n+\t    !(_clar.summary = clar_summary_init(_clar.summary_filename))) {\n+\t\tclar_print_onabort(\"Failed to open the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tif (clar_sandbox() < 0) {\n+\t\tclar_print_onabort(\"Failed to sandbox the test runner.\\n\");\n+\t\texit(-1);\n+\t}\n+}\n+\n+int\n+clar_test_run(void)\n+{\n+\tsize_t i;\n+\tstruct clar_explicit *explicit;\n+\n+\tif (_clar.explicit) {\n+\t\tfor (explicit = _clar.explicit; explicit; explicit = explicit->next)\n+\t\t\tclar_run_suite(&_clar_suites[explicit->suite_idx], explicit->filter);\n+\t} else {\n+\t\tfor (i = 0; i < _clar_suite_count; ++i)\n+\t\t\tclar_run_suite(&_clar_suites[i], NULL);\n+\t}\n+\n+\treturn _clar.total_errors;\n+}\n+\n+void\n+clar_test_shutdown(void)\n+{\n+\tstruct clar_explicit *explicit, *explicit_next;\n+\tstruct clar_report *report, *report_next;\n+\n+\tclar_print_shutdown(\n+\t\t_clar.tests_ran,\n+\t\t(int)_clar_suite_count,\n+\t\t_clar.total_errors\n+\t);\n+\n+\tclar_unsandbox();\n+\n+\tif (_clar.write_summary && clar_summary_shutdown(_clar.summary) < 0) {\n+\t\tclar_print_onabort(\"Failed to write the summary file\\n\");\n+\t\texit(-1);\n+\t}\n+\n+\tfor (explicit = _clar.explicit; explicit; explicit = explicit_next) {\n+\t\texplicit_next = explicit->next;\n+\t\tfree(explicit);\n+\t}\n+\n+\tfor (report = _clar.reports; report; report = report_next) {\n+\t\treport_next = report->next;\n+\t\tfree(report);\n+\t}\n+\n+\tfree(_clar.summary_filename);\n+}\n+\n+int\n+clar_test(int argc, char **argv)\n+{\n+\tint errors;\n+\n+\tclar_test_init(argc, argv);\n+\terrors = clar_test_run();\n+\tclar_test_shutdown();\n+\n+\treturn errors;\n+}\n+\n+static void abort_test(void)\n+{\n+\tif (!_clar.trampoline_enabled) {\n+\t\tclar_print_onabort(\n+\t\t\t\t\"Fatal error: a cleanup method raised an exception.\");\n+\t\tclar_report_errors(_clar.last_report);\n+\t\texit(-1);\n+\t}\n+\n+\tCL_TRACE(CL_TRACE__TEST__LONGJMP);\n+\tlongjmp(_clar.trampoline, -1);\n+}\n+\n+void clar__skip(void)\n+{\n+\t_clar.last_report->status = CL_TEST_SKIP;\n+\t_clar.total_skipped++;\n+\tabort_test();\n+}\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tstruct clar_error *error = calloc(1, sizeof(struct clar_error));\n+\n+\tif (_clar.last_report->errors == NULL)\n+\t\t_clar.last_report->errors = error;\n+\n+\tif (_clar.last_report->last_error != NULL)\n+\t\t_clar.last_report->last_error->next = error;\n+\n+\t_clar.last_report->last_error = error;\n+\n+\terror->file = file;\n+\terror->function = function;\n+\terror->line_number = line;\n+\terror->error_msg = error_msg;\n+\n+\tif (description != NULL)\n+\t\terror->description = strdup(description);\n+\n+\t_clar.total_errors++;\n+\t_clar.last_report->status = CL_TEST_FAILURE;\n+\n+\tif (should_abort)\n+\t\tabort_test();\n+}\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *error_msg,\n+\tconst char *description,\n+\tint should_abort)\n+{\n+\tif (condition)\n+\t\treturn;\n+\n+\tclar__fail(file, function, line, error_msg, description, should_abort);\n+}\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *function,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...)\n+{\n+\tva_list args;\n+\tchar buf[4096];\n+\tint is_equal = 1;\n+\n+\tva_start(args, fmt);\n+\n+\tif (!strcmp(\"%s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strcmp(s1, s2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && s1[pos] && s2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s' (at byte %d)\",\n+\t\t\t\t\ts1, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%s' != '%s'\", s1, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*s\", fmt)) {\n+\t\tconst char *s1 = va_arg(args, const char *);\n+\t\tconst char *s2 = va_arg(args, const char *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!s1 || !s2) ? (s1 == s2) : !strncmp(s1, s2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (s1 && s2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; s1[pos] == s2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s' (at byte %d)\",\n+\t\t\t\t\tlen, s1, len, s2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*s' != '%.*s'\", len, s1, len, s2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls' (at byte %d)\",\n+\t\t\t\t\twcs1, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%ls' != '%ls'\", wcs1, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if(!strcmp(\"%.*ls\", fmt)) {\n+\t\tconst wchar_t *wcs1 = va_arg(args, const wchar_t *);\n+\t\tconst wchar_t *wcs2 = va_arg(args, const wchar_t *);\n+\t\tint len = va_arg(args, int);\n+\t\tis_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);\n+\n+\t\tif (!is_equal) {\n+\t\t\tif (wcs1 && wcs2) {\n+\t\t\t\tint pos;\n+\t\t\t\tfor (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)\n+\t\t\t\t\t/* find differing byte offset */;\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls' (at byte %d)\",\n+\t\t\t\t\tlen, wcs1, len, wcs2, pos);\n+\t\t\t} else {\n+\t\t\t\tp_snprintf(buf, sizeof(buf), \"'%.*ls' != '%.*ls'\", len, wcs1, len, wcs2);\n+\t\t\t}\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%\"PRIuZ, fmt) || !strcmp(\"%\"PRIxZ, fmt)) {\n+\t\tsize_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);\n+\t\tis_equal = (sz1 == sz2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, sz1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, sz2);\n+\t\t}\n+\t}\n+\telse if (!strcmp(\"%p\", fmt)) {\n+\t\tvoid *p1 = va_arg(args, void *), *p2 = va_arg(args, void *);\n+\t\tis_equal = (p1 == p2);\n+\t\tif (!is_equal)\n+\t\t\tp_snprintf(buf, sizeof(buf), \"%p != %p\", p1, p2);\n+\t}\n+\telse {\n+\t\tint i1 = va_arg(args, int), i2 = va_arg(args, int);\n+\t\tis_equal = (i1 == i2);\n+\t\tif (!is_equal) {\n+\t\t\tint offset = p_snprintf(buf, sizeof(buf), fmt, i1);\n+\t\t\tstrncat(buf, \" != \", sizeof(buf) - offset);\n+\t\t\tp_snprintf(buf + offset + 4, sizeof(buf) - offset - 4, fmt, i2);\n+\t\t}\n+\t}\n+\n+\tva_end(args);\n+\n+\tif (!is_equal)\n+\t\tclar__fail(file, function, line, err, buf, should_abort);\n+}\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque)\n+{\n+\t_clar.local_cleanup = cleanup;\n+\t_clar.local_cleanup_payload = opaque;\n+}\n+\n+#include \"clar/sandbox.h\"\n+#include \"clar/fixtures.h\"\n+#include \"clar/fs.h\"\n+#include \"clar/print.h\"\n+#include \"clar/summary.h\"\ndiff --git a/t/unit-tests/clar/clar.h b/t/unit-tests/clar/clar.h\nnew file mode 100644\nindex 00000000000..8c22382bd56\n--- /dev/null\n+++ b/t/unit-tests/clar/clar.h\n@@ -0,0 +1,173 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST_H__\n+#define __CLAR_TEST_H__\n+\n+#include <stdlib.h>\n+\n+enum cl_test_status {\n+\tCL_TEST_OK,\n+\tCL_TEST_FAILURE,\n+\tCL_TEST_SKIP,\n+\tCL_TEST_NOTRUN,\n+};\n+\n+enum cl_output_format {\n+\tCL_OUTPUT_CLAP,\n+\tCL_OUTPUT_TAP,\n+};\n+\n+/** Setup clar environment */\n+void clar_test_init(int argc, char *argv[]);\n+int clar_test_run(void);\n+void clar_test_shutdown(void);\n+\n+/** One shot setup & run */\n+int clar_test(int argc, char *argv[]);\n+\n+const char *clar_sandbox_path(void);\n+\n+void cl_set_cleanup(void (*cleanup)(void *), void *opaque);\n+void cl_fs_cleanup(void);\n+\n+/**\n+ * cl_trace_* is a hook to provide a simple global tracing\n+ * mechanism.\n+ *\n+ * The goal here is to let main() provide clar-proper\n+ * with a callback to optionally write log info for\n+ * test operations into the same stream used by their\n+ * actual tests.  This would let them print test names\n+ * and maybe performance data as they choose.\n+ *\n+ * The goal is NOT to alter the flow of control or to\n+ * override test selection/skipping.  (So the callback\n+ * does not return a value.)\n+ *\n+ * The goal is NOT to duplicate the existing\n+ * pass/fail/skip reporting.  (So the callback\n+ * does not accept a status/errorcode argument.)\n+ *\n+ */\n+typedef enum cl_trace_event {\n+\tCL_TRACE__SUITE_BEGIN,\n+\tCL_TRACE__SUITE_END,\n+\tCL_TRACE__TEST__BEGIN,\n+\tCL_TRACE__TEST__END,\n+\tCL_TRACE__TEST__RUN_BEGIN,\n+\tCL_TRACE__TEST__RUN_END,\n+\tCL_TRACE__TEST__LONGJMP,\n+} cl_trace_event;\n+\n+typedef void (cl_trace_cb)(\n+\tcl_trace_event ev,\n+\tconst char *suite_name,\n+\tconst char *test_name,\n+\tvoid *payload);\n+\n+/**\n+ * Register a callback into CLAR to send global trace events.\n+ * Pass NULL to disable.\n+ */\n+void cl_trace_register(cl_trace_cb *cb, void *payload);\n+\n+\n+#ifdef CLAR_FIXTURE_PATH\n+const char *cl_fixture(const char *fixture_name);\n+void cl_fixture_sandbox(const char *fixture_name);\n+void cl_fixture_cleanup(const char *fixture_name);\n+const char *cl_fixture_basename(const char *fixture_name);\n+#endif\n+\n+/**\n+ * Assertion macros with explicit error message\n+ */\n+#define cl_must_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 1)\n+#define cl_must_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 1)\n+#define cl_assert_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 1)\n+\n+/**\n+ * Check macros with explicit error message\n+ */\n+#define cl_check_pass_(expr, desc) clar__assert((expr) >= 0, __FILE__, __func__, __LINE__, \"Function call failed: \" #expr, desc, 0)\n+#define cl_check_fail_(expr, desc) clar__assert((expr) < 0, __FILE__, __func__, __LINE__, \"Expected function call to fail: \" #expr, desc, 0)\n+#define cl_check_(expr, desc) clar__assert((expr) != 0, __FILE__, __func__, __LINE__, \"Expression is not true: \" #expr, desc, 0)\n+\n+/**\n+ * Assertion macros with no error message\n+ */\n+#define cl_must_pass(expr) cl_must_pass_(expr, NULL)\n+#define cl_must_fail(expr) cl_must_fail_(expr, NULL)\n+#define cl_assert(expr) cl_assert_(expr, NULL)\n+\n+/**\n+ * Check macros with no error message\n+ */\n+#define cl_check_pass(expr) cl_check_pass_(expr, NULL)\n+#define cl_check_fail(expr) cl_check_fail_(expr, NULL)\n+#define cl_check(expr) cl_check_(expr, NULL)\n+\n+/**\n+ * Forced failure/warning\n+ */\n+#define cl_fail(desc) clar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1)\n+#define cl_warning(desc) clar__fail(__FILE__, __func__, __LINE__, \"Warning during test execution:\", desc, 0)\n+\n+#define cl_skip() clar__skip()\n+\n+/**\n+ * Typed assertion macros\n+ */\n+#define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%s\", (s1), (s2))\n+#define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%s\", (s1), (s2))\n+\n+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%ls\", (wcs1), (wcs2))\n+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%ls\", (wcs1), (wcs2))\n+\n+#define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2, 1, \"%.*s\", (s1), (s2), (int)(len))\n+#define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #s1 \" != \" #s2 \" (\" #note \")\", 1, \"%.*s\", (s1), (s2), (int)(len))\n+\n+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2, 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__func__,__LINE__,\"String mismatch: \" #wcs1 \" != \" #wcs2 \" (\" #note \")\", 1, \"%.*ls\", (wcs1), (wcs2), (int)(len))\n+\n+#define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, \"%d\", (int)(i1), (int)(i2))\n+#define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2 \" (\" #note \")\", 1, \"%d\", (i1), (i2))\n+#define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__func__,__LINE__,#i1 \" != \" #i2, 1, (fmt), (int)(i1), (int)(i2))\n+\n+#define cl_assert_equal_b(b1,b2) clar__assert_equal(__FILE__,__func__,__LINE__,#b1 \" != \" #b2, 1, \"%d\", (int)((b1) != 0),(int)((b2) != 0))\n+\n+#define cl_assert_equal_p(p1,p2) clar__assert_equal(__FILE__,__func__,__LINE__,\"Pointer mismatch: \" #p1 \" != \" #p2, 1, \"%p\", (p1), (p2))\n+\n+void clar__skip(void);\n+\n+void clar__fail(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert(\n+\tint condition,\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *error,\n+\tconst char *description,\n+\tint should_abort);\n+\n+void clar__assert_equal(\n+\tconst char *file,\n+\tconst char *func,\n+\tsize_t line,\n+\tconst char *err,\n+\tint should_abort,\n+\tconst char *fmt,\n+\t...);\n+\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fixtures.h b/t/unit-tests/clar/clar/fixtures.h\nnew file mode 100644\nindex 00000000000..6ec6423484d\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fixtures.h\n@@ -0,0 +1,50 @@\n+#ifdef CLAR_FIXTURE_PATH\n+static const char *\n+fixture_path(const char *base, const char *fixture_name)\n+{\n+\tstatic char _path[4096];\n+\tsize_t root_len;\n+\n+\troot_len = strlen(base);\n+\tstrncpy(_path, base, sizeof(_path));\n+\n+\tif (_path[root_len - 1] != '/')\n+\t\t_path[root_len++] = '/';\n+\n+\tif (fixture_name[0] == '/')\n+\t\tfixture_name++;\n+\n+\tstrncpy(_path + root_len,\n+\t\tfixture_name,\n+\t\tsizeof(_path) - root_len);\n+\n+\treturn _path;\n+}\n+\n+const char *cl_fixture(const char *fixture_name)\n+{\n+\treturn fixture_path(CLAR_FIXTURE_PATH, fixture_name);\n+}\n+\n+void cl_fixture_sandbox(const char *fixture_name)\n+{\n+\tfs_copy(cl_fixture(fixture_name), _clar_path);\n+}\n+\n+const char *cl_fixture_basename(const char *fixture_name)\n+{\n+\tconst char *p;\n+\n+\tfor (p = fixture_name; *p; p++) {\n+\t\tif (p[0] == '/' && p[1] && p[1] != '/')\n+\t\t\tfixture_name = p+1;\n+\t}\n+\n+\treturn fixture_name;\n+}\n+\n+void cl_fixture_cleanup(const char *fixture_name)\n+{\n+\tfs_rm(fixture_path(_clar_path, cl_fixture_basename(fixture_name)));\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nnew file mode 100644\nindex 00000000000..3e39890bd3e\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -0,0 +1,522 @@\n+/*\n+ * By default, use a read/write loop to copy files on POSIX systems.\n+ * On Linux, use sendfile by default as it's slightly faster.  On\n+ * macOS, we avoid fcopyfile by default because it's slightly slower.\n+ */\n+#undef USE_FCOPYFILE\n+#define USE_SENDFILE 1\n+\n+#ifdef _WIN32\n+\n+#ifdef CLAR_WIN32_LONGPATHS\n+# define CLAR_MAX_PATH 4096\n+#else\n+# define CLAR_MAX_PATH MAX_PATH\n+#endif\n+\n+#define RM_RETRY_COUNT\t5\n+#define RM_RETRY_DELAY\t10\n+\n+#ifdef __MINGW32__\n+\n+/* These security-enhanced functions are not available\n+ * in MinGW, so just use the vanilla ones */\n+#define wcscpy_s(a, b, c) wcscpy((a), (c))\n+#define wcscat_s(a, b, c) wcscat((a), (c))\n+\n+#endif /* __MINGW32__ */\n+\n+static int\n+fs__dotordotdot(WCHAR *_tocheck)\n+{\n+\treturn _tocheck[0] == '.' &&\n+\t\t(_tocheck[1] == '\\0' ||\n+\t\t (_tocheck[1] == '.' && _tocheck[2] == '\\0'));\n+}\n+\n+static int\n+fs_rmdir_rmdir(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\n+\twhile (!RemoveDirectoryW(_wpath)) {\n+\t\t/* Only retry when we have retries remaining, and the\n+\t\t * error was ERROR_DIR_NOT_EMPTY. */\n+\t\tif (retries++ > RM_RETRY_COUNT ||\n+\t\t\tERROR_DIR_NOT_EMPTY != GetLastError())\n+\t\t\treturn -1;\n+\n+\t\t/* Give whatever has a handle to a child item some time\n+\t\t * to release it before trying again */\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void translate_path(WCHAR *path, size_t path_size)\n+{\n+    size_t path_len, i;\n+\n+    if (wcsncmp(path, L\"\\\\\\\\?\\\\\", 4) == 0)\n+\treturn;\n+\n+    path_len = wcslen(path);\n+    cl_assert(path_size > path_len + 4);\n+\n+    for (i = path_len; i > 0; i--) {\n+\tWCHAR c = path[i - 1];\n+\n+\tif (c == L'/')\n+\t    path[i + 3] = L'\\\\';\n+\telse\n+\t    path[i + 3] = path[i - 1];\n+    }\n+\n+    path[0] = L'\\\\';\n+    path[1] = L'\\\\';\n+    path[2] = L'?';\n+    path[3] = L'\\\\';\n+    path[path_len + 4] = L'\\0';\n+}\n+\n+static void\n+fs_rmdir_helper(WCHAR *_wsource)\n+{\n+\tWCHAR buffer[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buffer_prefix_len;\n+\n+\t/* Set up the buffer and capture the length */\n+\twcscpy_s(buffer, CLAR_MAX_PATH, _wsource);\n+\ttranslate_path(buffer, CLAR_MAX_PATH);\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"\\\\\");\n+\tbuffer_prefix_len = wcslen(buffer);\n+\n+\t/* FindFirstFile needs a wildcard to match multiple items */\n+\twcscat_s(buffer, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buffer, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buffer + buffer_prefix_len, CLAR_MAX_PATH - buffer_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_rmdir_helper(buffer);\n+\t\telse {\n+\t\t\t/* If set, the +R bit must be cleared before deleting */\n+\t\t\tif (FILE_ATTRIBUTE_READONLY & find_data.dwFileAttributes)\n+\t\t\t\tcl_assert(SetFileAttributesW(buffer, find_data.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\t\tcl_assert(DeleteFileW(buffer));\n+\t\t}\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+\n+\t/* Now that the directory is empty, remove it */\n+\tcl_assert(0 == fs_rmdir_rmdir(_wsource));\n+}\n+\n+static int\n+fs_rm_wait(WCHAR *_wpath)\n+{\n+\tunsigned retries = 1;\n+\tDWORD last_error;\n+\n+\tdo {\n+\t\tif (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(_wpath))\n+\t\t\tlast_error = GetLastError();\n+\t\telse\n+\t\t\tlast_error = ERROR_SUCCESS;\n+\n+\t\t/* Is the item gone? */\n+\t\tif (ERROR_FILE_NOT_FOUND == last_error ||\n+\t\t\tERROR_PATH_NOT_FOUND == last_error)\n+\t\t\treturn 0;\n+\n+\t\tSleep(RM_RETRY_DELAY * retries * retries);\n+\t}\n+\twhile (retries++ <= RM_RETRY_COUNT);\n+\n+\treturn -1;\n+}\n+\n+static void\n+fs_rm(const char *_source)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH];\n+\tDWORD attrs;\n+\n+\t/* The input path is UTF-8. Convert it to wide characters\n+\t * for use with the Windows API */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1, /* Indicates NULL termination */\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\n+\t/* Does the item exist? If not, we have no work to do */\n+\tattrs = GetFileAttributesW(wsource);\n+\n+\tif (INVALID_FILE_ATTRIBUTES == attrs)\n+\t\treturn;\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & attrs)\n+\t\tfs_rmdir_helper(wsource);\n+\telse {\n+\t\t/* The item is a file. Strip the +R bit */\n+\t\tif (FILE_ATTRIBUTE_READONLY & attrs)\n+\t\t\tcl_assert(SetFileAttributesW(wsource, attrs & ~FILE_ATTRIBUTE_READONLY));\n+\n+\t\tcl_assert(DeleteFileW(wsource));\n+\t}\n+\n+\t/* Wait for the DeleteFile or RemoveDirectory call to complete */\n+\tcl_assert(0 == fs_rm_wait(wsource));\n+}\n+\n+static void\n+fs_copydir_helper(WCHAR *_wsource, WCHAR *_wdest)\n+{\n+\tWCHAR buf_source[CLAR_MAX_PATH], buf_dest[CLAR_MAX_PATH];\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\tsize_t buf_source_prefix_len, buf_dest_prefix_len;\n+\n+\twcscpy_s(buf_source, CLAR_MAX_PATH, _wsource);\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_source, CLAR_MAX_PATH);\n+\tbuf_source_prefix_len = wcslen(buf_source);\n+\n+\twcscpy_s(buf_dest, CLAR_MAX_PATH, _wdest);\n+\twcscat_s(buf_dest, CLAR_MAX_PATH, L\"\\\\\");\n+\ttranslate_path(buf_dest, CLAR_MAX_PATH);\n+\tbuf_dest_prefix_len = wcslen(buf_dest);\n+\n+\t/* Get an enumerator for the items in the source. */\n+\twcscat_s(buf_source, CLAR_MAX_PATH, L\"*\");\n+\tfind_handle = FindFirstFileW(buf_source, &find_data);\n+\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\n+\t/* Create the target directory. */\n+\tcl_assert(CreateDirectoryW(_wdest, NULL));\n+\n+\tdo {\n+\t\t/* FindFirstFile/FindNextFile gives back . and ..\n+\t\t * entries at the beginning */\n+\t\tif (fs__dotordotdot(find_data.cFileName))\n+\t\t\tcontinue;\n+\n+\t\twcscpy_s(buf_source + buf_source_prefix_len, CLAR_MAX_PATH - buf_source_prefix_len, find_data.cFileName);\n+\t\twcscpy_s(buf_dest + buf_dest_prefix_len, CLAR_MAX_PATH - buf_dest_prefix_len, find_data.cFileName);\n+\n+\t\tif (FILE_ATTRIBUTE_DIRECTORY & find_data.dwFileAttributes)\n+\t\t\tfs_copydir_helper(buf_source, buf_dest);\n+\t\telse\n+\t\t\tcl_assert(CopyFileW(buf_source, buf_dest, TRUE));\n+\t}\n+\twhile (FindNextFileW(find_handle, &find_data));\n+\n+\t/* Ensure that we successfully completed the enumeration */\n+\tcl_assert(ERROR_NO_MORE_FILES == GetLastError());\n+\n+\t/* Close the find handle */\n+\tFindClose(find_handle);\n+}\n+\n+static void\n+fs_copy(const char *_source, const char *_dest)\n+{\n+\tWCHAR wsource[CLAR_MAX_PATH], wdest[CLAR_MAX_PATH];\n+\tDWORD source_attrs, dest_attrs;\n+\tHANDLE find_handle;\n+\tWIN32_FIND_DATAW find_data;\n+\n+\t/* The input paths are UTF-8. Convert them to wide characters\n+\t * for use with the Windows API. */\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_source,\n+\t\t\t\t-1,\n+\t\t\t\twsource,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\tcl_assert(MultiByteToWideChar(CP_UTF8,\n+\t\t\t\tMB_ERR_INVALID_CHARS,\n+\t\t\t\t_dest,\n+\t\t\t\t-1,\n+\t\t\t\twdest,\n+\t\t\t\tCLAR_MAX_PATH));\n+\n+\ttranslate_path(wsource, CLAR_MAX_PATH);\n+\ttranslate_path(wdest, CLAR_MAX_PATH);\n+\n+\t/* Check the source for existence */\n+\tsource_attrs = GetFileAttributesW(wsource);\n+\tcl_assert(INVALID_FILE_ATTRIBUTES != source_attrs);\n+\n+\t/* Check the target for existence */\n+\tdest_attrs = GetFileAttributesW(wdest);\n+\n+\tif (INVALID_FILE_ATTRIBUTES != dest_attrs) {\n+\t\t/* Target exists; append last path part of source to target.\n+\t\t * Use FindFirstFile to parse the path */\n+\t\tfind_handle = FindFirstFileW(wsource, &find_data);\n+\t\tcl_assert(INVALID_HANDLE_VALUE != find_handle);\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, L\"\\\\\");\n+\t\twcscat_s(wdest, CLAR_MAX_PATH, find_data.cFileName);\n+\t\tFindClose(find_handle);\n+\n+\t\t/* Check the new target for existence */\n+\t\tcl_assert(INVALID_FILE_ATTRIBUTES == GetFileAttributesW(wdest));\n+\t}\n+\n+\tif (FILE_ATTRIBUTE_DIRECTORY & source_attrs)\n+\t\tfs_copydir_helper(wsource, wdest);\n+\telse\n+\t\tcl_assert(CopyFileW(wsource, wdest, TRUE));\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+#ifdef CLAR_FIXTURE_PATH\n+\tfs_rm(fixture_path(_clar_path, \"*\"));\n+#endif\n+}\n+\n+#else\n+\n+#include <errno.h>\n+#include <string.h>\n+#include <limits.h>\n+#include <dirent.h>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/types.h>\n+#include <sys/stat.h>\n+\n+#if defined(__linux__)\n+# include <sys/sendfile.h>\n+#endif\n+\n+#if defined(__APPLE__)\n+# include <copyfile.h>\n+#endif\n+\n+static void basename_r(const char **out, int *out_len, const char *in)\n+{\n+\tsize_t in_len = strlen(in), start_pos;\n+\n+\tfor (in_len = strlen(in); in_len; in_len--) {\n+\t\tif (in[in_len - 1] != '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tfor (start_pos = in_len; start_pos; start_pos--) {\n+\t\tif (in[start_pos - 1] == '/')\n+\t\t\tbreak;\n+\t}\n+\n+\tcl_assert(in_len - start_pos < INT_MAX);\n+\n+\tif (in_len - start_pos > 0) {\n+\t\t*out = &in[start_pos];\n+\t\t*out_len = (in_len - start_pos);\n+\t} else {\n+\t\t*out = \"/\";\n+\t\t*out_len = 1;\n+\t}\n+}\n+\n+static char *joinpath(const char *dir, const char *base, int base_len)\n+{\n+\tchar *out;\n+\tint len;\n+\n+\tif (base_len == -1) {\n+\t\tsize_t bl = strlen(base);\n+\n+\t\tcl_assert(bl < INT_MAX);\n+\t\tbase_len = (int)bl;\n+\t}\n+\n+\tlen = strlen(dir) + base_len + 2;\n+\tcl_assert(len > 0);\n+\n+\tcl_assert(out = malloc(len));\n+\tcl_assert(snprintf(out, len, \"%s/%.*s\", dir, base_len, base) < len);\n+\n+\treturn out;\n+}\n+\n+static void\n+fs_copydir_helper(const char *source, const char *dest, int dest_mode)\n+{\n+\tDIR *source_dir;\n+\tstruct dirent *d;\n+\n+\tmkdir(dest, dest_mode);\n+\n+\tcl_assert_(source_dir = opendir(source), \"Could not open source dir\");\n+\twhile ((d = (errno = 0, readdir(source_dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(source, d->d_name, -1);\n+\t\tfs_copy(child, dest);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\n+\tclosedir(source_dir);\n+}\n+\n+static void\n+fs_copyfile_helper(const char *source, size_t source_len, const char *dest, int dest_mode)\n+{\n+\tint in, out;\n+\n+\tcl_must_pass((in = open(source, O_RDONLY)));\n+\tcl_must_pass((out = open(dest, O_WRONLY|O_CREAT|O_TRUNC, dest_mode)));\n+\n+#if USE_FCOPYFILE && defined(__APPLE__)\n+\t((void)(source_len)); /* unused */\n+\tcl_must_pass(fcopyfile(in, out, 0, COPYFILE_DATA));\n+#elif USE_SENDFILE && defined(__linux__)\n+\t{\n+\t\tssize_t ret = 0;\n+\n+\t\twhile (source_len && (ret = sendfile(out, in, NULL, source_len)) > 0) {\n+\t\t\tsource_len -= (size_t)ret;\n+\t\t}\n+\t\tcl_assert(ret >= 0);\n+\t}\n+#else\n+\t{\n+\t\tchar buf[131072];\n+\t\tssize_t ret;\n+\n+\t\t((void)(source_len)); /* unused */\n+\n+\t\twhile ((ret = read(in, buf, sizeof(buf))) > 0) {\n+\t\t\tsize_t len = (size_t)ret;\n+\n+\t\t\twhile (len && (ret = write(out, buf, len)) > 0) {\n+\t\t\t\tcl_assert(ret <= (ssize_t)len);\n+\t\t\t\tlen -= ret;\n+\t\t\t}\n+\t\t\tcl_assert(ret >= 0);\n+\t\t}\n+\t\tcl_assert(ret == 0);\n+\t}\n+#endif\n+\n+\tclose(in);\n+\tclose(out);\n+}\n+\n+static void\n+fs_copy(const char *source, const char *_dest)\n+{\n+\tchar *dbuf = NULL;\n+\tconst char *dest = NULL;\n+\tstruct stat source_st, dest_st;\n+\n+\tcl_must_pass_(lstat(source, &source_st), \"Failed to stat copy source\");\n+\n+\tif (lstat(_dest, &dest_st) == 0) {\n+\t\tconst char *base;\n+\t\tint base_len;\n+\n+\t\t/* Target exists and is directory; append basename */\n+\t\tcl_assert(S_ISDIR(dest_st.st_mode));\n+\n+\t\tbasename_r(&base, &base_len, source);\n+\t\tcl_assert(base_len < INT_MAX);\n+\n+\t\tdbuf = joinpath(_dest, base, base_len);\n+\t\tdest = dbuf;\n+\t} else if (errno != ENOENT) {\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t} else {\n+\t\tdest = _dest;\n+\t}\n+\n+\tif (S_ISDIR(source_st.st_mode)) {\n+\t\tfs_copydir_helper(source, dest, source_st.st_mode);\n+\t} else {\n+\t\tfs_copyfile_helper(source, source_st.st_size, dest, source_st.st_mode);\n+\t}\n+\n+\tfree(dbuf);\n+}\n+\n+static void\n+fs_rmdir_helper(const char *path)\n+{\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tcl_assert_(dir = opendir(path), \"Could not open dir\");\n+\twhile ((d = (errno = 0, readdir(dir))) != NULL) {\n+\t\tchar *child;\n+\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tchild = joinpath(path, d->d_name, -1);\n+\t\tfs_rm(child);\n+\t\tfree(child);\n+\t}\n+\n+\tcl_assert_(errno == 0, \"Failed to iterate source dir\");\n+\tclosedir(dir);\n+\n+\tcl_must_pass_(rmdir(path), \"Could not remove directory\");\n+}\n+\n+static void\n+fs_rm(const char *path)\n+{\n+\tstruct stat st;\n+\n+\tif (lstat(path, &st)) {\n+\t\tif (errno == ENOENT)\n+\t\t\treturn;\n+\n+\t\tcl_fail(\"Cannot copy; cannot stat destination\");\n+\t}\n+\n+\tif (S_ISDIR(st.st_mode)) {\n+\t\tfs_rmdir_helper(path);\n+\t} else {\n+\t\tcl_must_pass(unlink(path));\n+\t}\n+}\n+\n+void\n+cl_fs_cleanup(void)\n+{\n+\tclar_unsandbox();\n+\tclar_sandbox();\n+}\n+#endif\ndiff --git a/t/unit-tests/clar/clar/print.h b/t/unit-tests/clar/clar/print.h\nnew file mode 100644\nindex 00000000000..c17e2f693bd\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/print.h\n@@ -0,0 +1,211 @@\n+/* clap: clar protocol, the traditional clar output format */\n+\n+static void clar_print_clap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\tprintf(\"Loaded %d suites: %s\\n\", (int)suite_count, suite_names);\n+\tprintf(\"Started (test status codes: OK='.' FAILURE='F' SKIPPED='S')\\n\");\n+}\n+\n+static void clar_print_clap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"\\n\\n\");\n+\tclar_report_all();\n+}\n+\n+static void clar_print_clap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tprintf(\"  %d) Failure:\\n\", num);\n+\n+\tprintf(\"%s::%s [%s:%\"PRIuZ\"]\\n\",\n+\t\treport->suite,\n+\t\treport->test,\n+\t\terror->file,\n+\t\terror->line_number);\n+\n+\tprintf(\"  %s\\n\", error->error_msg);\n+\n+\tif (error->description != NULL)\n+\t\tprintf(\"  %s\\n\", error->description);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_clap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tif (_clar.verbosity > 1) {\n+\t\tprintf(\"%s::%s: \", suite_name, test_name);\n+\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\"ok\\n\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"fail\\n\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"skipped\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"notrun\"); break;\n+\t\t}\n+\t} else {\n+\t\tswitch (status) {\n+\t\tcase CL_TEST_OK: printf(\".\"); break;\n+\t\tcase CL_TEST_FAILURE: printf(\"F\"); break;\n+\t\tcase CL_TEST_SKIP: printf(\"S\"); break;\n+\t\tcase CL_TEST_NOTRUN: printf(\"N\"); break;\n+\t\t}\n+\n+\t\tfflush(stdout);\n+\t}\n+}\n+\n+static void clar_print_clap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tif (_clar.verbosity == 1)\n+\t\tprintf(\"\\n%s\", suite_name);\n+\n+\t(void)suite_index;\n+}\n+\n+static void clar_print_clap_onabort(const char *fmt, va_list arg)\n+{\n+\tvfprintf(stderr, fmt, arg);\n+}\n+\n+/* tap: test anywhere protocol format */\n+\n+static void clar_print_tap_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\t(void)test_count;\n+\t(void)suite_count;\n+\t(void)suite_names;\n+\tprintf(\"TAP version 13\\n\");\n+}\n+\n+static void clar_print_tap_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\t(void)suite_count;\n+\t(void)error_count;\n+\n+\tprintf(\"1..%d\\n\", test_count);\n+}\n+\n+static void clar_print_tap_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\t(void)num;\n+\t(void)report;\n+\t(void)error;\n+}\n+\n+static void print_escaped(const char *str)\n+{\n+\tchar *c;\n+\n+\twhile ((c = strchr(str, '\\'')) != NULL) {\n+\t\tprintf(\"%.*s\", (int)(c - str), str);\n+\t\tprintf(\"''\");\n+\t\tstr = c + 1;\n+\t}\n+\n+\tprintf(\"%s\", str);\n+}\n+\n+static void clar_print_tap_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tconst struct clar_error *error = _clar.last_report->errors;\n+\n+\t(void)test_name;\n+\t(void)test_number;\n+\n+\tswitch(status) {\n+\tcase CL_TEST_OK:\n+\t\tprintf(\"ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\tcase CL_TEST_FAILURE:\n+\t\tprintf(\"not ok %d - %s::%s\\n\", test_number, suite_name, test_name);\n+\n+\t\tprintf(\"    ---\\n\");\n+\t\tprintf(\"    reason: |\\n\");\n+\t\tprintf(\"      %s\\n\", error->error_msg);\n+\n+\t\tif (error->description)\n+\t\t\tprintf(\"      %s\\n\", error->description);\n+\n+\t\tprintf(\"    at:\\n\");\n+\t\tprintf(\"      file: '\"); print_escaped(error->file); printf(\"'\\n\");\n+\t\tprintf(\"      line: %\" PRIuZ \"\\n\", error->line_number);\n+\t\tprintf(\"      function: '%s'\\n\", error->function);\n+\t\tprintf(\"    ---\\n\");\n+\n+\t\tbreak;\n+\tcase CL_TEST_SKIP:\n+\tcase CL_TEST_NOTRUN:\n+\t\tprintf(\"ok %d - # SKIP %s::%s\\n\", test_number, suite_name, test_name);\n+\t\tbreak;\n+\t}\n+\n+\tfflush(stdout);\n+}\n+\n+static void clar_print_tap_onsuite(const char *suite_name, int suite_index)\n+{\n+\tprintf(\"# start of suite %d: %s\\n\", suite_index, suite_name);\n+}\n+\n+static void clar_print_tap_onabort(const char *fmt, va_list arg)\n+{\n+\tprintf(\"Bail out! \");\n+\tvprintf(fmt, arg);\n+\tfflush(stdout);\n+}\n+\n+/* indirection between protocol output selection */\n+\n+#define PRINT(FN, ...) do { \\\n+\t\tswitch (_clar.output_format) { \\\n+\t\t\tcase CL_OUTPUT_CLAP: \\\n+\t\t\t\tclar_print_clap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tcase CL_OUTPUT_TAP: \\\n+\t\t\t\tclar_print_tap_##FN (__VA_ARGS__); \\\n+\t\t\t\tbreak; \\\n+\t\t\tdefault: \\\n+\t\t\t\tabort(); \\\n+\t\t} \\\n+\t} while (0)\n+\n+static void clar_print_init(int test_count, int suite_count, const char *suite_names)\n+{\n+\tPRINT(init, test_count, suite_count, suite_names);\n+}\n+\n+static void clar_print_shutdown(int test_count, int suite_count, int error_count)\n+{\n+\tPRINT(shutdown, test_count, suite_count, error_count);\n+}\n+\n+static void clar_print_error(int num, const struct clar_report *report, const struct clar_error *error)\n+{\n+\tPRINT(error, num, report, error);\n+}\n+\n+static void clar_print_ontest(const char *suite_name, const char *test_name, int test_number, enum cl_test_status status)\n+{\n+\tPRINT(ontest, suite_name, test_name, test_number, status);\n+}\n+\n+static void clar_print_onsuite(const char *suite_name, int suite_index)\n+{\n+\tPRINT(onsuite, suite_name, suite_index);\n+}\n+\n+static void clar_print_onabort(const char *msg, ...)\n+{\n+\tva_list argp;\n+\tva_start(argp, msg);\n+\tPRINT(onabort, msg, argp);\n+\tva_end(argp);\n+}\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nnew file mode 100644\nindex 00000000000..7c177f35258\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -0,0 +1,153 @@\n+#ifdef __APPLE__\n+#include <sys/syslimits.h>\n+#endif\n+\n+static char _clar_path[4096 + 1];\n+\n+static int\n+is_valid_tmp_path(const char *path)\n+{\n+\tSTAT_T st;\n+\n+\tif (stat(path, &st) != 0)\n+\t\treturn 0;\n+\n+\tif (!S_ISDIR(st.st_mode))\n+\t\treturn 0;\n+\n+\treturn (access(path, W_OK) == 0);\n+}\n+\n+static int\n+find_tmp_path(char *buffer, size_t length)\n+{\n+#ifndef _WIN32\n+\tstatic const size_t var_count = 5;\n+\tstatic const char *env_vars[] = {\n+\t\t\"CLAR_TMP\", \"TMPDIR\", \"TMP\", \"TEMP\", \"USERPROFILE\"\n+\t};\n+\n+\tsize_t i;\n+\n+\tfor (i = 0; i < var_count; ++i) {\n+\t\tconst char *env = getenv(env_vars[i]);\n+\t\tif (!env)\n+\t\t\tcontinue;\n+\n+\t\tif (is_valid_tmp_path(env)) {\n+#ifdef __APPLE__\n+\t\t\tif (length >= PATH_MAX && realpath(env, buffer) != NULL)\n+\t\t\t\treturn 0;\n+#endif\n+\t\t\tstrncpy(buffer, env, length - 1);\n+\t\t\tbuffer[length - 1] = '\\0';\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\t/* If the environment doesn't say anything, try to use /tmp */\n+\tif (is_valid_tmp_path(\"/tmp\")) {\n+#ifdef __APPLE__\n+\t\tif (length >= PATH_MAX && realpath(\"/tmp\", buffer) != NULL)\n+\t\t\treturn 0;\n+#endif\n+\t\tstrncpy(buffer, \"/tmp\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+#else\n+\tDWORD env_len = GetEnvironmentVariable(\"CLAR_TMP\", buffer, (DWORD)length);\n+\tif (env_len > 0 && env_len < (DWORD)length)\n+\t\treturn 0;\n+\n+\tif (GetTempPath((DWORD)length, buffer))\n+\t\treturn 0;\n+#endif\n+\n+\t/* This system doesn't like us, try to use the current directory */\n+\tif (is_valid_tmp_path(\".\")) {\n+\t\tstrncpy(buffer, \".\", length - 1);\n+\t\tbuffer[length - 1] = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\treturn -1;\n+}\n+\n+static void clar_unsandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0')\n+\t\treturn;\n+\n+\tcl_must_pass(chdir(\"..\"));\n+\n+\tfs_rm(_clar_path);\n+}\n+\n+static int build_sandbox_path(void)\n+{\n+#ifdef CLAR_TMPDIR\n+\tconst char path_tail[] = CLAR_TMPDIR \"_XXXXXX\";\n+#else\n+\tconst char path_tail[] = \"clar_tmp_XXXXXX\";\n+#endif\n+\n+\tsize_t len;\n+\n+\tif (find_tmp_path(_clar_path, sizeof(_clar_path)) < 0)\n+\t\treturn -1;\n+\n+\tlen = strlen(_clar_path);\n+\n+#ifdef _WIN32\n+\t{ /* normalize path to POSIX forward slashes */\n+\t\tsize_t i;\n+\t\tfor (i = 0; i < len; ++i) {\n+\t\t\tif (_clar_path[i] == '\\\\')\n+\t\t\t\t_clar_path[i] = '/';\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (_clar_path[len - 1] != '/') {\n+\t\t_clar_path[len++] = '/';\n+\t}\n+\n+\tstrncpy(_clar_path + len, path_tail, sizeof(_clar_path) - len);\n+\n+#if defined(__MINGW32__)\n+\tif (_mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(_WIN32)\n+\tif (_mktemp_s(_clar_path, sizeof(_clar_path)) != 0)\n+\t\treturn -1;\n+\n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#else\n+\tif (mkdtemp(_clar_path) == NULL)\n+\t\treturn -1;\n+#endif\n+\n+\treturn 0;\n+}\n+\n+static int clar_sandbox(void)\n+{\n+\tif (_clar_path[0] == '\\0' && build_sandbox_path() < 0)\n+\t\treturn -1;\n+\n+\tif (chdir(_clar_path) != 0)\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const char *clar_sandbox_path(void)\n+{\n+\treturn _clar_path;\n+}\ndiff --git a/t/unit-tests/clar/clar/summary.h b/t/unit-tests/clar/clar/summary.h\nnew file mode 100644\nindex 00000000000..4dd352e28b8\n--- /dev/null\n+++ b/t/unit-tests/clar/clar/summary.h\n@@ -0,0 +1,143 @@\n+\n+#include <stdio.h>\n+#include <time.h>\n+\n+static int clar_summary_close_tag(\n+    struct clar_summary *summary, const char *tag, int indent)\n+{\n+\tconst char *indt;\n+\n+\tif (indent == 0) indt = \"\";\n+\telse if (indent == 1) indt = \"\\t\";\n+\telse indt = \"\\t\\t\";\n+\n+\treturn fprintf(summary->fp, \"%s</%s>\\n\", indt, tag);\n+}\n+\n+static int clar_summary_testsuites(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"<testsuites>\\n\");\n+}\n+\n+static int clar_summary_testsuite(struct clar_summary *summary,\n+    int idn, const char *name, time_t timestamp,\n+    int test_count, int fail_count, int error_count)\n+{\n+\tstruct tm *tm = localtime(&timestamp);\n+\tchar iso_dt[20];\n+\n+\tif (strftime(iso_dt, sizeof(iso_dt), \"%Y-%m-%dT%H:%M:%S\", tm) == 0)\n+\t\treturn -1;\n+\n+\treturn fprintf(summary->fp, \"\\t<testsuite\"\n+\t\t       \" id=\\\"%d\\\"\"\n+\t\t       \" name=\\\"%s\\\"\"\n+\t\t       \" hostname=\\\"localhost\\\"\"\n+\t\t       \" timestamp=\\\"%s\\\"\"\n+\t\t       \" tests=\\\"%d\\\"\"\n+\t\t       \" failures=\\\"%d\\\"\"\n+\t\t       \" errors=\\\"%d\\\">\\n\",\n+\t\t       idn, name, iso_dt, test_count, fail_count, error_count);\n+}\n+\n+static int clar_summary_testcase(struct clar_summary *summary,\n+    const char *name, const char *classname, double elapsed)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t<testcase name=\\\"%s\\\" classname=\\\"%s\\\" time=\\\"%.2f\\\">\\n\",\n+\t\tname, classname, elapsed);\n+}\n+\n+static int clar_summary_failure(struct clar_summary *summary,\n+    const char *type, const char *message, const char *desc)\n+{\n+\treturn fprintf(summary->fp,\n+\t    \"\\t\\t\\t<failure type=\\\"%s\\\"><![CDATA[%s\\n%s]]></failure>\\n\",\n+\t    type, message, desc);\n+}\n+\n+static int clar_summary_skipped(struct clar_summary *summary)\n+{\n+\treturn fprintf(summary->fp, \"\\t\\t\\t<skipped />\\n\");\n+}\n+\n+struct clar_summary *clar_summary_init(const char *filename)\n+{\n+\tstruct clar_summary *summary;\n+\tFILE *fp;\n+\n+\tif ((fp = fopen(filename, \"w\")) == NULL) {\n+\t\tperror(\"fopen\");\n+\t\treturn NULL;\n+\t}\n+\n+\tif ((summary = malloc(sizeof(struct clar_summary))) == NULL) {\n+\t\tperror(\"malloc\");\n+\t\tfclose(fp);\n+\t\treturn NULL;\n+\t}\n+\n+\tsummary->filename = filename;\n+\tsummary->fp = fp;\n+\n+\treturn summary;\n+}\n+\n+int clar_summary_shutdown(struct clar_summary *summary)\n+{\n+\tstruct clar_report *report;\n+\tconst char *last_suite = NULL;\n+\n+\tif (clar_summary_testsuites(summary) < 0)\n+\t\tgoto on_error;\n+\n+\treport = _clar.reports;\n+\twhile (report != NULL) {\n+\t\tstruct clar_error *error = report->errors;\n+\n+\t\tif (last_suite == NULL || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_testsuite(summary, 0, report->suite,\n+\t\t\t    report->start, _clar.tests_ran, _clar.total_errors, 0) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\n+\t\tlast_suite = report->suite;\n+\n+\t\tclar_summary_testcase(summary, report->test, report->suite, report->elapsed);\n+\n+\t\twhile (error != NULL) {\n+\t\t\tif (clar_summary_failure(summary, \"assert\",\n+\t\t\t    error->error_msg, error->description) < 0)\n+\t\t\t\tgoto on_error;\n+\n+\t\t\terror = error->next;\n+\t\t}\n+\n+\t\tif (report->status == CL_TEST_SKIP)\n+\t\t\tclar_summary_skipped(summary);\n+\n+\t\tif (clar_summary_close_tag(summary, \"testcase\", 2) < 0)\n+\t\t\tgoto on_error;\n+\n+\t\treport = report->next;\n+\n+\t\tif (!report || strcmp(last_suite, report->suite) != 0) {\n+\t\t\tif (clar_summary_close_tag(summary, \"testsuite\", 1) < 0)\n+\t\t\t\tgoto on_error;\n+\t\t}\n+\t}\n+\n+\tif (clar_summary_close_tag(summary, \"testsuites\", 0) < 0 ||\n+\t    fclose(summary->fp) != 0)\n+\t\tgoto on_error;\n+\n+\tprintf(\"written summary file to %s\\n\", summary->filename);\n+\n+\tfree(summary);\n+\treturn 0;\n+\n+on_error:\n+\tfclose(summary->fp);\n+\tfree(summary);\n+\treturn -1;\n+}\ndiff --git a/t/unit-tests/clar/generate.py b/t/unit-tests/clar/generate.py\nnew file mode 100755\nindex 00000000000..80996ac3e71\n--- /dev/null\n+++ b/t/unit-tests/clar/generate.py\n@@ -0,0 +1,266 @@\n+#!/usr/bin/env python\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+from __future__ import with_statement\n+from string import Template\n+import re, fnmatch, os, sys, codecs, pickle\n+\n+class Module(object):\n+    class Template(object):\n+        def __init__(self, module):\n+            self.module = module\n+\n+        def _render_callback(self, cb):\n+            if not cb:\n+                return '    { NULL, NULL }'\n+            return '    { \"%s\", &%s }' % (cb['short_name'], cb['symbol'])\n+\n+    class DeclarationTemplate(Template):\n+        def render(self):\n+            out = \"\\n\".join(\"extern %s;\" % cb['declaration'] for cb in self.module.callbacks) + \"\\n\"\n+\n+            for initializer in self.module.initializers:\n+                out += \"extern %s;\\n\" % initializer['declaration']\n+\n+            if self.module.cleanup:\n+                out += \"extern %s;\\n\" % self.module.cleanup['declaration']\n+\n+            return out\n+\n+    class CallbacksTemplate(Template):\n+        def render(self):\n+            out = \"static const struct clar_func _clar_cb_%s[] = {\\n\" % self.module.name\n+            out += \",\\n\".join(self._render_callback(cb) for cb in self.module.callbacks)\n+            out += \"\\n};\\n\"\n+            return out\n+\n+    class InfoTemplate(Template):\n+        def render(self):\n+            templates = []\n+\n+            initializers = self.module.initializers\n+            if len(initializers) == 0:\n+                initializers = [ None ]\n+\n+            for initializer in initializers:\n+                name = self.module.clean_name()\n+                if initializer and initializer['short_name'].startswith('initialize_'):\n+                    variant = initializer['short_name'][len('initialize_'):]\n+                    name += \" (%s)\" % variant.replace('_', ' ')\n+\n+                template = Template(\n+            r\"\"\"\n+    {\n+        \"${clean_name}\",\n+    ${initialize},\n+    ${cleanup},\n+        ${cb_ptr}, ${cb_count}, ${enabled}\n+    }\"\"\"\n+                ).substitute(\n+                    clean_name = name,\n+                    initialize = self._render_callback(initializer),\n+                    cleanup = self._render_callback(self.module.cleanup),\n+                    cb_ptr = \"_clar_cb_%s\" % self.module.name,\n+                    cb_count = len(self.module.callbacks),\n+                    enabled = int(self.module.enabled)\n+                )\n+                templates.append(template)\n+\n+            return ','.join(templates)\n+\n+    def __init__(self, name):\n+        self.name = name\n+\n+        self.mtime = None\n+        self.enabled = True\n+        self.modified = False\n+\n+    def clean_name(self):\n+        return self.name.replace(\"_\", \"::\")\n+\n+    def _skip_comments(self, text):\n+        SKIP_COMMENTS_REGEX = re.compile(\n+            r'//.*?$|/\\*.*?\\*/|\\'(?:\\\\.|[^\\\\\\'])*\\'|\"(?:\\\\.|[^\\\\\"])*\"',\n+            re.DOTALL | re.MULTILINE)\n+\n+        def _replacer(match):\n+            s = match.group(0)\n+            return \"\" if s.startswith('/') else s\n+\n+        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)\n+\n+    def parse(self, contents):\n+        TEST_FUNC_REGEX = r\"^(void\\s+(test_%s__(\\w+))\\s*\\(\\s*void\\s*\\))\\s*\\{\"\n+\n+        contents = self._skip_comments(contents)\n+        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)\n+\n+        self.callbacks = []\n+        self.initializers = []\n+        self.cleanup = None\n+\n+        for (declaration, symbol, short_name) in regex.findall(contents):\n+            data = {\n+                \"short_name\" : short_name,\n+                \"declaration\" : declaration,\n+                \"symbol\" : symbol\n+            }\n+\n+            if short_name.startswith('initialize'):\n+                self.initializers.append(data)\n+            elif short_name == 'cleanup':\n+                self.cleanup = data\n+            else:\n+                self.callbacks.append(data)\n+\n+        return self.callbacks != []\n+\n+    def refresh(self, path):\n+        self.modified = False\n+\n+        try:\n+            st = os.stat(path)\n+\n+            # Not modified\n+            if st.st_mtime == self.mtime:\n+                return True\n+\n+            self.modified = True\n+            self.mtime = st.st_mtime\n+\n+            with codecs.open(path, encoding='utf-8') as fp:\n+                raw_content = fp.read()\n+\n+        except IOError:\n+            return False\n+\n+        return self.parse(raw_content)\n+\n+class TestSuite(object):\n+\n+    def __init__(self, path, output):\n+        self.path = path\n+        self.output = output\n+\n+    def should_generate(self, path):\n+        if not os.path.isfile(path):\n+            return True\n+\n+        if any(module.modified for module in self.modules.values()):\n+            return True\n+\n+        return False\n+\n+    def find_modules(self):\n+        modules = []\n+        for root, _, files in os.walk(self.path):\n+            module_root = root[len(self.path):]\n+            module_root = [c for c in module_root.split(os.sep) if c]\n+\n+            tests_in_module = fnmatch.filter(files, \"*.c\")\n+\n+            for test_file in tests_in_module:\n+                full_path = os.path.join(root, test_file)\n+                module_name = \"_\".join(module_root + [test_file[:-2]]).replace(\"-\", \"_\")\n+\n+                modules.append((full_path, module_name))\n+\n+        return modules\n+\n+    def load_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        cache = {}\n+\n+        try:\n+            fp = open(path, 'rb')\n+            cache = pickle.load(fp)\n+            fp.close()\n+        except (IOError, ValueError):\n+            pass\n+\n+        return cache\n+\n+    def save_cache(self):\n+        path = os.path.join(self.output, '.clarcache')\n+        with open(path, 'wb') as cache:\n+            pickle.dump(self.modules, cache)\n+\n+    def load(self, force = False):\n+        module_data = self.find_modules()\n+        self.modules = {} if force else self.load_cache()\n+\n+        for path, name in module_data:\n+            if name not in self.modules:\n+                self.modules[name] = Module(name)\n+\n+            if not self.modules[name].refresh(path):\n+                del self.modules[name]\n+\n+    def disable(self, excluded):\n+        for exclude in excluded:\n+            for module in self.modules.values():\n+                name = module.clean_name()\n+                if name.startswith(exclude):\n+                    module.enabled = False\n+                    module.modified = True\n+\n+    def suite_count(self):\n+        return sum(max(1, len(m.initializers)) for m in self.modules.values())\n+\n+    def callback_count(self):\n+        return sum(len(module.callbacks) for module in self.modules.values())\n+\n+    def write(self):\n+        output = os.path.join(self.output, 'clar.suite')\n+\n+        if not self.should_generate(output):\n+            return False\n+\n+        with open(output, 'w') as data:\n+            modules = sorted(self.modules.values(), key=lambda module: module.name)\n+\n+            for module in modules:\n+                t = Module.DeclarationTemplate(module)\n+                data.write(t.render())\n+\n+            for module in modules:\n+                t = Module.CallbacksTemplate(module)\n+                data.write(t.render())\n+\n+            suites = \"static struct clar_suite _clar_suites[] = {\" + ','.join(\n+                Module.InfoTemplate(module).render() for module in modules\n+            ) + \"\\n};\\n\"\n+\n+            data.write(suites)\n+\n+            data.write(\"static const size_t _clar_suite_count = %d;\\n\" % self.suite_count())\n+            data.write(\"static const size_t _clar_callback_count = %d;\\n\" % self.callback_count())\n+\n+        self.save_cache()\n+        return True\n+\n+if __name__ == '__main__':\n+    from optparse import OptionParser\n+\n+    parser = OptionParser()\n+    parser.add_option('-f', '--force', action=\"store_true\", dest='force', default=False)\n+    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])\n+    parser.add_option('-o', '--output', dest='output')\n+\n+    options, args = parser.parse_args()\n+    if len(args) > 1:\n+        print(\"More than one path given\")\n+        sys.exit(1)\n+\n+    path = args.pop() if args else '.'\n+    output = options.output or path\n+    suite = TestSuite(path, output)\n+    suite.load(options.force)\n+    suite.disable(options.excluded)\n+    if suite.write():\n+        print(\"Written `clar.suite` (%d tests in %d suites)\" % (suite.callback_count(), suite.suite_count()))\ndiff --git a/t/unit-tests/clar/test/.gitignore b/t/unit-tests/clar/test/.gitignore\nnew file mode 100644\nindex 00000000000..a477d0c40ca\n--- /dev/null\n+++ b/t/unit-tests/clar/test/.gitignore\n@@ -0,0 +1,4 @@\n+clar.suite\n+.clarcache\n+clar_test\n+*.o\ndiff --git a/t/unit-tests/clar/test/Makefile b/t/unit-tests/clar/test/Makefile\nnew file mode 100644\nindex 00000000000..93c6b2ad32c\n--- /dev/null\n+++ b/t/unit-tests/clar/test/Makefile\n@@ -0,0 +1,39 @@\n+#\n+# Copyright (c) Vicent Marti. All rights reserved.\n+#\n+# This file is part of clar, distributed under the ISC license.\n+# For full terms see the included COPYING file.\n+#\n+\n+#\n+# Set up the path to the clar sources and to the fixtures directory\n+#\n+# The fixture path needs to be an absolute path so it can be used\n+# even after we have chdir'ed into the test directory while testing.\n+#\n+CURRENT_MAKEFILE  := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))\n+TEST_DIRECTORY    := $(abspath $(dir $(CURRENT_MAKEFILE)))\n+CLAR_PATH         := $(dir $(TEST_DIRECTORY))\n+CLAR_FIXTURE_PATH := $(TEST_DIRECTORY)/resources/\n+\n+CFLAGS=-g -I.. -I. -Wall -DCLAR_FIXTURE_PATH=\\\"$(CLAR_FIXTURE_PATH)\\\"\n+\n+.PHONY: clean\n+\n+# list the objects that go into our test\n+objects = main.o sample.o\n+\n+# build the test executable itself\n+clar_test: $(objects) clar_test.h clar.suite $(CLAR_PATH)clar.c\n+\t$(CC) $(CFLAGS) -o $@ \"$(CLAR_PATH)clar.c\" $(objects)\n+\n+# test object files depend on clar macros\n+$(objects) : $(CLAR_PATH)clar.h\n+\n+# build the clar.suite file of test metadata\n+clar.suite:\n+\tpython \"$(CLAR_PATH)generate.py\" .\n+\n+# remove all generated files\n+clean:\n+\t$(RM) -rf *.o clar.suite .clarcache clar_test clar_test.dSYM\ndiff --git a/t/unit-tests/clar/test/clar_test.h b/t/unit-tests/clar/test/clar_test.h\nnew file mode 100644\nindex 00000000000..0fcaa639aa8\n--- /dev/null\n+++ b/t/unit-tests/clar/test/clar_test.h\n@@ -0,0 +1,16 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+#ifndef __CLAR_TEST__\n+#define __CLAR_TEST__\n+\n+/* Import the standard clar helper functions */\n+#include \"clar.h\"\n+\n+/* Your custom shared includes / defines here */\n+extern int global_test_counter;\n+\n+#endif\ndiff --git a/t/unit-tests/clar/test/main.c b/t/unit-tests/clar/test/main.c\nnew file mode 100644\nindex 00000000000..59e56ad255b\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c\n@@ -0,0 +1,40 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Sample main() for clar tests.\n+ *\n+ * You should write your own main routine for clar tests that does specific\n+ * setup and teardown as necessary for your application.  The only required\n+ * line is the call to `clar_test(argc, argv)`, which will execute the test\n+ * suite.  If you want to check the return value of the test application,\n+ * your main() should return the same value returned by clar_test().\n+ */\n+\n+int global_test_counter = 0;\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\tint ret;\n+\n+\t/* Your custom initialization here */\n+\tglobal_test_counter = 0;\n+\n+\t/* Run the test suite */\n+\tret = clar_test(argc, argv);\n+\n+\t/* Your custom cleanup here */\n+\tcl_assert_equal_i(8, global_test_counter);\n+\n+\treturn ret;\n+}\ndiff --git a/t/unit-tests/clar/test/main.c.sample b/t/unit-tests/clar/test/main.c.sample\nnew file mode 100644\nindex 00000000000..a4d91b72fa8\n--- /dev/null\n+++ b/t/unit-tests/clar/test/main.c.sample\n@@ -0,0 +1,27 @@\n+/*\n+ * Copyright (c) Vicent Marti. All rights reserved.\n+ *\n+ * This file is part of clar, distributed under the ISC license.\n+ * For full terms see the included COPYING file.\n+ */\n+\n+#include \"clar_test.h\"\n+\n+/*\n+ * Minimal main() for clar tests.\n+ *\n+ * Modify this with any application specific setup or teardown that you need.\n+ * The only required line is the call to `clar_test(argc, argv)`, which will\n+ * execute the test suite.  If you want to check the return value of the test\n+ * application, main() should return the same value returned by clar_test().\n+ */\n+\n+#ifdef _WIN32\n+int __cdecl main(int argc, char *argv[])\n+#else\n+int main(int argc, char *argv[])\n+#endif\n+{\n+\t/* Run the test suite */\n+\treturn clar_test(argc, argv);\n+}\ndiff --git a/t/unit-tests/clar/test/resources/test/file b/t/unit-tests/clar/test/resources/test/file\nnew file mode 100644\nindex 00000000000..220f4aa98a7\n--- /dev/null\n+++ b/t/unit-tests/clar/test/resources/test/file\n@@ -0,0 +1 @@\n+File\ndiff --git a/t/unit-tests/clar/test/sample.c b/t/unit-tests/clar/test/sample.c\nnew file mode 100644\nindex 00000000000..faa1209262f\n--- /dev/null\n+++ b/t/unit-tests/clar/test/sample.c\n@@ -0,0 +1,84 @@\n+#include \"clar_test.h\"\n+#include <sys/stat.h>\n+\n+static int file_size(const char *filename)\n+{\n+\tstruct stat st;\n+\n+\tif (stat(filename, &st) == 0)\n+\t\treturn (int)st.st_size;\n+\treturn -1;\n+}\n+\n+void test_sample__initialize(void)\n+{\n+\tglobal_test_counter++;\n+}\n+\n+void test_sample__cleanup(void)\n+{\n+\tcl_fixture_cleanup(\"test\");\n+\n+\tcl_assert(file_size(\"test/file\") == -1);\n+}\n+\n+void test_sample__1(void)\n+{\n+\tcl_assert(1);\n+\tcl_must_pass(0);  /* 0 == success */\n+\tcl_must_fail(-1); /* <0 == failure */\n+\tcl_must_pass(-1); /* demonstrate a failing call */\n+}\n+\n+void test_sample__2(void)\n+{\n+\tcl_fixture_sandbox(\"test\");\n+\n+\tcl_assert(file_size(\"test/nonexistent\") == -1);\n+\tcl_assert(file_size(\"test/file\") > 0);\n+\tcl_assert(100 == 101);\n+}\n+\n+void test_sample__strings(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_s(\"expected\", actual);\n+\tcl_assert_equal_s_(\"expected\", actual, \"second try with annotation\");\n+\tcl_assert_equal_s_(\"mismatched\", actual, \"this one fails\");\n+}\n+\n+void test_sample__strings_with_length(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_strn(\"expected_\", actual, 8);\n+\tcl_assert_equal_strn(\"exactly\", actual, 2);\n+\tcl_assert_equal_strn_(\"expected_\", actual, 8, \"with annotation\");\n+\tcl_assert_equal_strn_(\"exactly\", actual, 3, \"this one fails\");\n+}\n+\n+void test_sample__int(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i(100, value);\n+\tcl_assert_equal_i_(101, value, \"extra note on failing test\");\n+}\n+\n+void test_sample__int_fmt(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_i_fmt(022, value, \"%04o\");\n+}\n+\n+void test_sample__bool(void)\n+{\n+\tint value = 100;\n+\tcl_assert_equal_b(1, value);       /* test equality as booleans */\n+\tcl_assert_equal_b(0, value);\n+}\n+\n+void test_sample__ptr(void)\n+{\n+\tconst char *actual = \"expected\";\n+\tcl_assert_equal_p(actual, actual); /* pointers to same object */\n+\tcl_assert_equal_p(&actual, actual);\n+}\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502119","messageId":"f24401f0a87059f4ff0c15ca0cfae4a899299b44.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 03/14] t/clar: fix compatibility with NonStop","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:16:51Z","receivedAt":"2024-09-04T14:16:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The NonStop platform does not have `mkdtemp()` available, which we rely\non in `build_sandbox_path()`. Fix this issue by using `mktemp()` and\n`mkdir()` instead on this platform.\n\nThis has been cherry-picked from the upstream pull request at [1].\n\n[1]: https://github.com/clar-test/clar/pull/96\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/sandbox.h | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/sandbox.h b/t/unit-tests/clar/clar/sandbox.h\nindex 7c177f35258..e25057b7c49 100644\n--- a/t/unit-tests/clar/clar/sandbox.h\n+++ b/t/unit-tests/clar/clar/sandbox.h\n@@ -120,6 +120,12 @@ static int build_sandbox_path(void)\n \tif (_mktemp(_clar_path) == NULL)\n \t\treturn -1;\n \n+\tif (mkdir(_clar_path, 0700) != 0)\n+\t\treturn -1;\n+#elif defined(__TANDEM)\n+\tif (mktemp(_clar_path) == NULL)\n+\t\treturn -1;\n+\n \tif (mkdir(_clar_path, 0700) != 0)\n \t\treturn -1;\n #elif defined(_WIN32)\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502121","messageId":"658a601c5411f38457007ed961582b5d898f813a.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 04/14] clar: avoid compile error with mingw-w64","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:16:54Z","receivedAt":"2024-09-04T14:16:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen using mingw-w64 to compile the code, and using `_stat()`, it is\nnecessary to use `struct _stat`, too, and not `struct stat` (as the\nlatter is incompatible with the \"dashed\" version because it is limited\nto 32-bit time types for backwards compatibility).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nindex 3fc2c768158..e2ebe551d38 100644\n--- a/t/unit-tests/clar/clar.c\n+++ b/t/unit-tests/clar/clar.c\n@@ -68,7 +68,7 @@\n #\t\tdefine PRIxZ \"Ix\"\n #\tendif\n \n-#\tif defined(_MSC_VER) || defined(__MINGW32__)\n+#\tif defined(_MSC_VER) || (defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))\n \ttypedef struct stat STAT_T;\n #\telse\n \ttypedef struct _stat STAT_T;\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502122","messageId":"0b8a6ac5fedf95e3d5328bf1fef75710e26ee3c3.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 05/14] clar(win32): avoid compile error due to unused `fs_copy()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:16:56Z","receivedAt":"2024-09-04T14:17:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen CLAR_FIXTURE_PATH is unset, the `fs_copy()` function seems not to\nbe used. But it is declared as `static`, and GCC does not like that,\ncomplaining that it should not be declared/defined to begin with.\n\nWe could mark this function as (potentially) unused by following the\n`MAYBE_UNUSED` pattern from Git's `git-compat-util.h`. However, this is\na GCC-only construct that is not understood by Visual C. Besides, `clar`\ndoes not use that pattern at all.\n\nInstead, let's use the `((void)SYMBOL);` pattern that `clar` already\nuses elsewhere; This avoids the compile error by sorta kinda make the\nfunction used after a fashion.\n\nNote: GCC 14.x (which Git for Windows' SDK already uses) is able to\nfigure out that this function is unused even though there are recursive\ncalls between `fs_copy()` and `fs_copydir_helper()`; Earlier GCC\nversions do not detect that, and therefore the issue has been hidden\nfrom the regular Linux CI builds (where GCC 14.x is not yet used). That\nis the reason why this change is only made in the Windows-specific\nportion of `t/unit-tests/clar/clar/fs.h`.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar/fs.h | 2 ++\n 1 file changed, 2 insertions(+)\n\ndiff --git a/t/unit-tests/clar/clar/fs.h b/t/unit-tests/clar/clar/fs.h\nindex 3e39890bd3e..8b206179fc4 100644\n--- a/t/unit-tests/clar/clar/fs.h\n+++ b/t/unit-tests/clar/clar/fs.h\n@@ -297,6 +297,8 @@ cl_fs_cleanup(void)\n {\n #ifdef CLAR_FIXTURE_PATH\n \tfs_rm(fixture_path(_clar_path, \"*\"));\n+#else\n+\t((void)fs_copy); /* unused */\n #endif\n }\n \n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502123","messageId":"c50e7a0ea68ba6fb4ef669403f79d3217d242f9f.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 06/14] clar: stop including `shellapi.h` unnecessarily","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:16:59Z","receivedAt":"2024-09-04T14:17:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThe `shellapi.h` header was included as of\nhttps://github.com/clar-test/clar/commit/136e763211aa, to have\n`SHFileOperation()` declared so that it could be called.\n\nHowever, https://github.com/clar-test/clar/commit/5ce31b69b525 removed\nthat call, and therefore that `#include <shellapi.h>` is unnecessary.\n\nIt is also unwanted in Git because this project uses a subset of Git for\nWindows' SDK in its CI builds that (for bandwidth reasons) excludes tons\nof header files, including `shellapi.h`.\n\nSo let's remove it.\n\nNote: Since the `windows.h` header would include `shellapi.h` anyway, we\nalso define `WIN32_LEAN_AND_MEAN` to avoid this and similar other\nunnecessary includes before including `windows.h`.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/clar/clar.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/unit-tests/clar/clar.c b/t/unit-tests/clar/clar.c\nindex e2ebe551d38..cef0f023c24 100644\n--- a/t/unit-tests/clar/clar.c\n+++ b/t/unit-tests/clar/clar.c\n@@ -19,9 +19,9 @@\n #include <sys/stat.h>\n \n #ifdef _WIN32\n+#\tdefine WIN32_LEAN_AND_MEAN\n #\tinclude <windows.h>\n #\tinclude <io.h>\n-#\tinclude <shellapi.h>\n #\tinclude <direct.h>\n \n #\tdefine _MAIN_CC __cdecl\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502124","messageId":"b8f3f16dd2776b889922f7d07ee5d448268fac65.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 07/14] Makefile: fix sparse dependency on GENERATED_H","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:17:04Z","receivedAt":"2024-09-04T14:17:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"check\" Makefile target is essentially an alias around the \"sparse\"\ntarget. The one difference though is that it will tell users to instead\nrun the \"test\" target in case they do not have sparse(1) installed, as\nchances are high that they wanted to execute the test suite rather than\ndoing semantic checks.\n\nBut even though the \"check\" target ultimately just ends up executing\n`make sparse`, it still depends on our generated headers. This does not\nmake any sense though: they are irrelevant for the \"test\" target advice,\nand if these headers are required for the \"sparse\" target they must be\ndeclared as a dependency on the aliased target, not the alias.\n\nBut even moving the dependency to the \"sparse\" target is wrong, as\nconcurrent builds may then end up generating the headers and running\nsparse concurrently. Instead, we make them a dependency of the specific\nobjects. While that is overly broad, it does ensure correct ordering.\nThe alternative, specifying which file depends on what generated header\nexplicitly, feels rather unmaintainable.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 28742a60964..efd305ab358 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3254,7 +3254,7 @@ check-sha1:: t/helper/test-tool$X\n \n SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n \n-$(SP_OBJ): %.sp: %.c %.o\n+$(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n \t\t-Wsparse-error \\\n \t\t$(SPARSE_FLAGS) $(SP_EXTRA_FLAGS) $< && \\\n@@ -3295,7 +3295,7 @@ style:\n \tgit clang-format --style file --diff --extensions c,h\n \n .PHONY: check\n-check: $(GENERATED_H)\n+check:\n \t@if sparse; \\\n \tthen \\\n \t\techo >&2 \"Use 'make sparse' instead\"; \\\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502125","messageId":"3d3fe443b9a3f368287efa709cd744583da358e1.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 08/14] Makefile: make hdr-check depend on generated headers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:17:06Z","receivedAt":"2024-09-04T14:17:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"hdr-check\" Makefile target compiles each of our headers as a\nstandalone code unit to ensure that they are not missing any type\ndeclarations and can be included standalone.\n\nWith the next commit we will wire up the clar unit testing framework,\nwhich will have the effect that some headers start depending on\ngenerated ones. While we could declare that dependency explicitly, it\ndoes not really feel very maintainable in the future.\n\nInstead, we do the same as in the preceding commit and have the objects\ndepend on all of our generated headers. While again overly broad, it is\neasy to maintain and generating headers is not an expensive thing to do\nanyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex efd305ab358..8c4487dd0c6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3284,7 +3284,7 @@ HCC = $(HCO:hco=hcc)\n \t@echo '#include \"git-compat-util.h\"' >$@\n \t@echo '#include \"$<\"' >>$@\n \n-$(HCO): %.hco: %.hcc FORCE\n+$(HCO): %.hco: %.hcc $(GENERATED_H) FORCE\n \t$(QUIET_HDR)$(CC) $(ALL_CFLAGS) -o /dev/null -c -xc $<\n \n .PHONY: hdr-check $(HCO)\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502126","messageId":"7d0f494850a48e5285e8823ff9252fdcf531439e.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 09/14] Makefile: do not use sparse on third-party sources","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:17:09Z","receivedAt":"2024-09-04T14:17:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have several third-party sources in our codebase that we have\nimported from upstream projects. These sources are mostly excluded from\nour static analysis, for example when running Coccinelle.\n\nDo the same for our \"sparse\" target by filtering them out.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 8c4487dd0c6..81a47b61327 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -3252,7 +3252,8 @@ t/helper/test-%$X: t/helper/test-%.o GIT-LDFLAGS $(GITLIBS) $(REFTABLE_TEST_LIB)\n check-sha1:: t/helper/test-tool$X\n \tt/helper/test-sha1.sh\n \n-SP_OBJ = $(patsubst %.o,%.sp,$(OBJECTS))\n+SP_SRC = $(filter-out $(THIRD_PARTY_SOURCES),$(patsubst %.o,%.c,$(OBJECTS)))\n+SP_OBJ = $(patsubst %.c,%.sp,$(SP_SRC))\n \n $(SP_OBJ): %.sp: %.c %.o $(GENERATED_H)\n \t$(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) \\\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502127","messageId":"9c74c5ae01989659c0347d2742f820d2161d274b.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 10/14] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:17:12Z","receivedAt":"2024-09-04T14:17:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Wire up the clar unit testing framework by introducing a new\n\"unit-tests\" executable. In contrast to the existing framework, this\nwill result in a single executable for all test suites. The ability to\npick specific tests to execute is retained via functionality built into\nthe clar itself.\n\nNote that we need to be a bit careful about how we need to invalidate\nour Makefile rules. While we obviously have to regenerate the clar suite\nwhen our test suites change, we also have to invalidate it in case any\nof the test suites gets removed. We do so by using our typical pattern\nof creating a `GIT-TEST-SUITES` file that gets updated whenever the set\nof test suites changes, so that we can easily depend on that file.\n\nAnother specialty is that we generate a \"clar-decls.h\" file. The test\nfunctions are neither static, nor do they have external declarations.\nThis is because they are getting parsed via \"generate.py\", which then\ncreates the external generations that get populated into an array. These\ndeclarations are only seen by the main function though.\n\nThe consequence is that we will get a bunch of \"missing prototypes\"\nerrors from our compiler for each of these test functions. To fix those\nerrors, we extract the `extern` declarations from \"clar.suite\" and put\nthem into a standalone header that then gets included by each of our\nunit tests. This gets rid of compiler warnings for every function which\nhas been extracted by \"generate.py\". More importantly though, it does\n_not_ get rid of warnings in case a function really isn't being used by\nanything. Thus, it would cause a compiler error if a function name was\nmistyped and thus not picked up by \"generate.py\".\n\nThe test driver \"unit-test.c\" is an empty stub for now. It will get\nimplemented in the next commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Makefile                       | 36 +++++++++++++++++++++---\n t/Makefile                     |  1 +\n t/unit-tests/.gitignore        |  2 ++\n t/unit-tests/clar-generate.awk | 50 ++++++++++++++++++++++++++++++++++\n t/unit-tests/unit-test.c       |  6 ++++\n t/unit-tests/unit-test.h       |  3 ++\n 7 files changed, 95 insertions(+), 4 deletions(-)\n create mode 100644 t/unit-tests/clar-generate.awk\n create mode 100644 t/unit-tests/unit-test.c\n create mode 100644 t/unit-tests/unit-test.h\n\ndiff --git a/.gitignore b/.gitignore\nindex 8caf3700c23..6687bd6db4c 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -9,6 +9,7 @@\n /GIT-PYTHON-VARS\n /GIT-SCRIPT-DEFINES\n /GIT-SPATCH-DEFINES\n+/GIT-TEST-SUITES\n /GIT-USER-AGENT\n /GIT-VERSION-FILE\n /bin-wrappers/\ndiff --git a/Makefile b/Makefile\nindex 81a47b61327..e38146b5eb0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -914,6 +914,8 @@ REFTABLE_TEST_LIB = reftable/libreftable_test.a\n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n GENERATED_H += hook-list.h\n+GENERATED_H += $(UNIT_TEST_DIR)/clar-decls.h\n+GENERATED_H += $(UNIT_TEST_DIR)/clar.suite\n \n .PHONY: generated-hdrs\n generated-hdrs: $(GENERATED_H)\n@@ -1334,6 +1336,11 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n+UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n+UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n+\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n@@ -2714,6 +2721,7 @@ OBJECTS += $(XDIFF_OBJS)\n OBJECTS += $(FUZZ_OBJS)\n OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n OBJECTS += $(UNIT_TEST_OBJS)\n+OBJECTS += $(UNIT_TESTS_OBJS)\n \n ifndef NO_CURL\n \tOBJECTS += http.o http-walker.o remote-curl.o\n@@ -3216,7 +3224,7 @@ endif\n \n test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))\n \n-all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)\n+all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n \n bin-wrappers/%: wrap-for-bin.sh\n \t$(call mkdir_p_parent_template)\n@@ -3648,7 +3656,7 @@ endif\n \n artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \\\n \t\tGIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \\\n-\t\t$(UNIT_TEST_PROGS) $(MOFILES)\n+\t\t$(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) $(MOFILES)\n \t$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \\\n \t\tSHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'\n \ttest -n \"$(ARTIFACTS_DIRECTORY)\"\n@@ -3704,6 +3712,7 @@ cocciclean:\n \n clean: profile-clean coverage-clean cocciclean\n \t$(RM) -r .build $(UNIT_TEST_BIN)\n+\t$(RM) GIT-TEST-SUITES\n \t$(RM) po/git.pot po/git-core.pot\n \t$(RM) git.res\n \t$(RM) $(OBJECTS)\n@@ -3863,7 +3872,26 @@ $(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o \\\n \t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \\\n \t\t$(filter %.o,$^) $(filter %.a,$^) $(LIBS)\n \n+GIT-TEST-SUITES: FORCE\n+\t@FLAGS='$(UNIT_TESTS_SUITES)'; \\\n+\t    if test x\"$$FLAGS\" != x\"`cat GIT-TEST-SUITES 2>/dev/null`\" ; then \\\n+\t\techo >&2 \"    * new test suites\"; \\\n+\t\techo \"$$FLAGS\" >GIT-TEST-SUITES; \\\n+            fi\n+\n+$(UNIT_TEST_DIR)/clar-decls.h: $(patsubst %,$(UNIT_TEST_DIR)/%.c,$(UNIT_TESTS_SUITES)) GIT-TEST-SUITES\n+\t$(QUIET_GEN)for suite in $(UNIT_TESTS_SUITES); do \\\n+\t\tsed -ne \"s/^\\(void test_$${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*(void)$$\\)/extern \\1;/p\" $(UNIT_TEST_DIR)/$$suite.c; \\\n+\tdone >$@\n+$(UNIT_TEST_DIR)/clar.suite: $(UNIT_TEST_DIR)/clar-decls.h\n+\t$(QUIET_GEN)awk -f $(UNIT_TEST_DIR)/clar-generate.awk $< >$(UNIT_TEST_DIR)/clar.suite\n+$(UNIT_TESTS_OBJS): $(UNIT_TEST_DIR)/clar-decls.h\n+$(UNIT_TESTS_OBJS): EXTRA_CPPFLAGS = -I$(UNIT_TEST_DIR)\n+$(UNIT_TESTS_PROG): $(UNIT_TEST_DIR)/clar.suite $(UNIT_TESTS_OBJS) $(GITLIBS) GIT-LDFLAGS\n+\t$(call mkdir_p_parent_template)\n+\t$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)\n+\n .PHONY: build-unit-tests unit-tests\n-build-unit-tests: $(UNIT_TEST_PROGS)\n-unit-tests: $(UNIT_TEST_PROGS) t/helper/test-tool$X\n+build-unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG)\n+unit-tests: $(UNIT_TEST_PROGS) $(UNIT_TESTS_PROG) t/helper/test-tool$X\n \t$(MAKE) -C t/ unit-tests\ndiff --git a/t/Makefile b/t/Makefile\nindex d2212de0b78..131ffd778fe 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -48,6 +48,7 @@ CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.tes\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n+UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n UNIT_TESTS = $(sort $(UNIT_TEST_PROGRAMS))\n UNIT_TESTS_NO_DIR = $(notdir $(UNIT_TESTS))\n \ndiff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore\nindex 5e56e040ec0..d0632ec7f9e 100644\n--- a/t/unit-tests/.gitignore\n+++ b/t/unit-tests/.gitignore\n@@ -1 +1,3 @@\n /bin\n+/clar.suite\n+/clar-decls.h\ndiff --git a/t/unit-tests/clar-generate.awk b/t/unit-tests/clar-generate.awk\nnew file mode 100644\nindex 00000000000..ab71ce6c9fc\n--- /dev/null\n+++ b/t/unit-tests/clar-generate.awk\n@@ -0,0 +1,50 @@\n+function add_suite(suite, initialize, cleanup, count) {\n+       if (!suite) return\n+       suite_count++\n+       callback_count += count\n+       suites = suites \"    {\\n\"\n+       suites = suites \"        \\\"\" suite \"\\\",\\n\"\n+       suites = suites \"        \" initialize \",\\n\"\n+       suites = suites \"        \" cleanup \",\\n\"\n+       suites = suites \"        _clar_cb_\" suite \", \" count \", 1\\n\"\n+       suites = suites \"    },\\n\"\n+}\n+\n+BEGIN {\n+       suites = \"static struct clar_suite _clar_suites[] = {\\n\"\n+}\n+\n+{\n+       print\n+       name = $3; sub(/\\(.*$/, \"\", name)\n+       suite = name; sub(/^test_/, \"\", suite); sub(/__.*$/, \"\", suite)\n+       short_name = name; sub(/^.*__/, \"\", short_name)\n+       cb = \"{ \\\"\" short_name \"\\\", &\" name \" }\"\n+       if (suite != prev_suite) {\n+               add_suite(prev_suite, initialize, cleanup, count)\n+               if (callbacks) callbacks = callbacks \"};\\n\"\n+               callbacks = callbacks \"static const struct clar_func _clar_cb_\" suite \"[] = {\\n\"\n+               initialize = \"{ NULL, NULL }\"\n+               cleanup = \"{ NULL, NULL }\"\n+               count = 0\n+               prev_suite = suite\n+       }\n+       if (short_name == \"initialize\") {\n+               initialize = cb\n+       } else if (short_name == \"cleanup\") {\n+               cleanup = cb\n+       } else {\n+               callbacks = callbacks \"    \" cb \",\\n\"\n+               count++\n+       }\n+}\n+\n+END {\n+       add_suite(suite, initialize, cleanup, count)\n+       suites = suites \"};\"\n+       if (callbacks) callbacks = callbacks \"};\"\n+       print callbacks\n+       print suites\n+       print \"static const size_t _clar_suite_count = \" suite_count \";\"\n+       print \"static const size_t _clar_callback_count = \" callback_count \";\"\n+}\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nnew file mode 100644\nindex 00000000000..3d12cde6dae\n--- /dev/null\n+++ b/t/unit-tests/unit-test.c\n@@ -0,0 +1,6 @@\n+#include \"unit-test.h\"\n+\n+int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+{\n+\treturn 0;\n+}\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nnew file mode 100644\nindex 00000000000..66ec2387cc6\n--- /dev/null\n+++ b/t/unit-tests/unit-test.h\n@@ -0,0 +1,3 @@\n+#include \"git-compat-util.h\"\n+#include \"clar/clar.h\"\n+#include \"clar-decls.h\"\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502128","messageId":"81d932bfa3328f9e876c0f51b4625974cda21471.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 11/14] t/unit-tests: implement test driver","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:17:15Z","receivedAt":"2024-09-04T14:17:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The test driver in \"unit-test.c\" is responsible for setting up our unit\ntests and eventually running them. As such, it is also responsible for\nparsing the command line arguments.\n\nThe clar unit testing framework provides function `clar_test()` that\nparses command line arguments and then executes the tests for us. In\ntheory that would already be sufficient. We have the special requirement\nto always generate TAP-formatted output though, so we'd have to always\npass the \"-t\" argument to clar. Furthermore, some of the options exposed\nby clar are ineffective when \"-t\" is used, but they would still be shown\nwhen the user passes the \"-h\" parameter to have the clar show its usage.\n\nImplement our own option handling instead of using the one provided by\nclar, which gives us greater flexibility in how exactly we set things\nup.\n\nWe would ideally not use any \"normal\" code of ours for this such that\nthe unit testing framework doesn't depend on it working correctly. But\nit is somewhat dubious whether we really want to reimplement all of the\noption parsing. So for now, let's be pragmatic and reuse it until we\nfind a good reason in the future why we'd really want to avoid it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/unit-test.c | 45 ++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 43 insertions(+), 2 deletions(-)\n\ndiff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c\nindex 3d12cde6dae..a474cdcfd35 100644\n--- a/t/unit-tests/unit-test.c\n+++ b/t/unit-tests/unit-test.c\n@@ -1,6 +1,47 @@\n #include \"unit-test.h\"\n+#include \"parse-options.h\"\n+#include \"string-list.h\"\n+#include \"strvec.h\"\n \n-int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+static const char * const unit_test_usage[] = {\n+\tN_(\"unit-test [<options>]\"),\n+\tNULL,\n+};\n+\n+int cmd_main(int argc, const char **argv)\n {\n-\treturn 0;\n+\tstruct string_list run_args = STRING_LIST_INIT_NODUP;\n+\tstruct string_list exclude_args = STRING_LIST_INIT_NODUP;\n+\tint immediate = 0;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL('i', \"immediate\", &immediate,\n+\t\t\t N_(\"immediately exit upon the first failed test\")),\n+\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"suite[::test]\"),\n+\t\t\t\tN_(\"run only test suite or individual test <suite[::test]>\")),\n+\t\tOPT_STRING_LIST('x', \"exclude\", &exclude_args, N_(\"suite\"),\n+\t\t\t\tN_(\"exclude test suite <suite>\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct strvec args = STRVEC_INIT;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, NULL, options,\n+\t\t\t     unit_test_usage, PARSE_OPT_KEEP_ARGV0);\n+\tif (argc > 1)\n+\t\tusagef(_(\"extra command line parameter '%s'\"), argv[0]);\n+\n+\tstrvec_push(&args, argv[0]);\n+\tstrvec_push(&args, \"-t\");\n+\tif (immediate)\n+\t\tstrvec_push(&args, \"-Q\");\n+\tfor (size_t i = 0; i < run_args.nr; i++)\n+\t\tstrvec_pushf(&args, \"-s%s\", run_args.items[i].string);\n+\tfor (size_t i = 0; i < exclude_args.nr; i++)\n+\t\tstrvec_pushf(&args, \"-x%s\", exclude_args.items[i].string);\n+\n+\tret = clar_test(args.nr, (char **) args.v);\n+\n+\tstring_list_clear(&run_args, 0);\n+\tstrvec_clear(&args);\n+\treturn ret;\n }\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502129","messageId":"604303e31aad3a9e74f7bdddc84f11d4d137c864.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 12/14] t/unit-tests: convert strvec tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:17:17Z","receivedAt":"2024-09-04T14:17:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the strvec tests to use the new clar unit testing framework.\nThis is a first test balloon that demonstrates how the testing infra for\nclar-based tests looks like.\n\nThe tests are part of the \"t/unit-tests/bin/unit-tests\" binary. When\nrunning that binary with an injected error, it generates TAP output:\n\n    # ./t/unit-tests/bin/unit-tests\n    TAP version 13\n    # start of suite 1: strvec\n    ok 1 - strvec::init\n    ok 2 - strvec::dynamic_init\n    ok 3 - strvec::clear\n    not ok 4 - strvec::push\n        ---\n        reason: |\n          String mismatch: (&vec)->v[i] != expect[i]\n          'foo' != 'fo' (at byte 2)\n        at:\n          file: 't/unit-tests/strvec.c'\n          line: 48\n          function: 'test_strvec__push'\n        ---\n    ok 5 - strvec::pushf\n    ok 6 - strvec::pushl\n    ok 7 - strvec::pushv\n    ok 8 - strvec::replace_at_head\n    ok 9 - strvec::replace_at_tail\n    ok 10 - strvec::replace_in_between\n    ok 11 - strvec::replace_with_substring\n    ok 12 - strvec::remove_at_head\n    ok 13 - strvec::remove_at_tail\n    ok 14 - strvec::remove_in_between\n    ok 15 - strvec::pop_empty_array\n    ok 16 - strvec::pop_non_empty_array\n    ok 17 - strvec::split_empty_string\n    ok 18 - strvec::split_single_item\n    ok 19 - strvec::split_multiple_items\n    ok 20 - strvec::split_whitespace_only\n    ok 21 - strvec::split_multiple_consecutive_whitespaces\n    ok 22 - strvec::detach\n    1..22\n\nThe binary also supports some parameters that allow us to run only a\nsubset of unit tests or alter the output:\n\n    $ ./t/unit-tests/bin/unit-tests -h\n    Usage: ./t/unit-tests/bin/unit-tests [options]\n\n    Options:\n      -sname        Run only the suite with `name` (can go to individual test name)\n      -iname        Include the suite with `name`\n      -xname        Exclude the suite with `name`\n      -v            Increase verbosity (show suite names)\n      -q            Only report tests that had an error\n      -Q            Quit as soon as a test fails\n      -t            Display results in tap format\n      -l            Print suite names\n      -r[filename]  Write summary file (to the optional filename)\n\nFurthermore, running `make unit-tests` runs the binary along with all\nthe other unit tests we have.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                |   2 +-\n t/unit-tests/strvec.c   | 241 ++++++++++++++++++++++++++++++++++++++++\n t/unit-tests/t-strvec.c | 211 -----------------------------------\n 3 files changed, 242 insertions(+), 212 deletions(-)\n create mode 100644 t/unit-tests/strvec.c\n delete mode 100644 t/unit-tests/t-strvec.c\n\ndiff --git a/Makefile b/Makefile\nindex e38146b5eb0..56ce6c00e44 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n@@ -1356,7 +1357,6 @@ UNIT_TEST_PROGRAMS += t-reftable-record\n UNIT_TEST_PROGRAMS += t-reftable-tree\n UNIT_TEST_PROGRAMS += t-strbuf\n UNIT_TEST_PROGRAMS += t-strcmp-offset\n-UNIT_TEST_PROGRAMS += t-strvec\n UNIT_TEST_PROGRAMS += t-trailer\n UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))\n UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))\ndiff --git a/t/unit-tests/strvec.c b/t/unit-tests/strvec.c\nnew file mode 100644\nindex 00000000000..bf4c0cb172e\n--- /dev/null\n+++ b/t/unit-tests/strvec.c\n@@ -0,0 +1,241 @@\n+#include \"unit-test.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+#define check_strvec(vec, ...) \\\n+\tdo { \\\n+\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n+\t\tsize_t expect_len = ARRAY_SIZE(expect); \\\n+\t\tcl_assert(expect_len > 0); \\\n+\t\tcl_assert_equal_p(expect[expect_len - 1], NULL); \\\n+\t\tcl_assert_equal_i((vec)->nr, expect_len - 1); \\\n+\t\tcl_assert((vec)->nr <= (vec)->alloc); \\\n+\t\tfor (size_t i = 0; i < expect_len; i++) \\\n+\t\t\tcl_assert_equal_s((vec)->v[i], expect[i]); \\\n+\t} while (0)\n+\n+void test_strvec__init(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__dynamic_init(void)\n+{\n+\tstruct strvec vec;\n+\n+\tstrvec_init(&vec);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__clear(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\tstrvec_clear(&vec);\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+}\n+\n+void test_strvec__push(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\n+\tstrvec_push(&vec, \"bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushf(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushf(&vec, \"foo: %d\", 1);\n+\tcheck_strvec(&vec, \"foo: 1\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushl(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pushv(void)\n+{\n+\tconst char *strings[] = {\n+\t\t\"foo\", \"bar\", \"baz\", NULL,\n+\t};\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushv(&vec, strings);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 0, \"replaced\");\n+\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 2, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_replace(&vec, 1, \"replaced\");\n+\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__replace_with_substring(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", NULL);\n+\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n+\tcheck_strvec(&vec, \"oo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_at_head(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 0);\n+\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_at_tail(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 2);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__remove_in_between(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_remove(&vec, 1);\n+\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pop_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__pop_non_empty_array(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_pop(&vec);\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_empty_string(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \"\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_single_item(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \"foo\");\n+\tcheck_strvec(&vec, \"foo\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_multiple_items(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \"foo bar baz\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_whitespace_only(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \" \\t\\n\");\n+\tcheck_strvec(&vec, NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__split_multiple_consecutive_whitespaces(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\n+\tstrvec_split(&vec, \"foo\\n\\t bar\");\n+\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n+\tstrvec_clear(&vec);\n+}\n+\n+void test_strvec__detach(void)\n+{\n+\tstruct strvec vec = STRVEC_INIT;\n+\tconst char **detached;\n+\n+\tstrvec_push(&vec, \"foo\");\n+\n+\tdetached = strvec_detach(&vec);\n+\tcl_assert_equal_s(detached[0], \"foo\");\n+\tcl_assert_equal_p(detached[1], NULL);\n+\n+\tcl_assert_equal_p(vec.v, empty_strvec);\n+\tcl_assert_equal_i(vec.nr, 0);\n+\tcl_assert_equal_i(vec.alloc, 0);\n+\n+\tfree((char *) detached[0]);\n+\tfree(detached);\n+}\ndiff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c\ndeleted file mode 100644\nindex c4bac8fc91b..00000000000\n--- a/t/unit-tests/t-strvec.c\n+++ /dev/null\n@@ -1,211 +0,0 @@\n-#include \"test-lib.h\"\n-#include \"strbuf.h\"\n-#include \"strvec.h\"\n-\n-#define check_strvec(vec, ...) \\\n-\tdo { \\\n-\t\tconst char *expect[] = { __VA_ARGS__ }; \\\n-\t\tif (check_uint(ARRAY_SIZE(expect), >, 0) && \\\n-\t\t    check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \\\n-\t\t    check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \\\n-\t\t    check_uint((vec)->nr, <=, (vec)->alloc)) { \\\n-\t\t\tfor (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \\\n-\t\t\t\tif (!check_str((vec)->v[i], expect[i])) { \\\n-\t\t\t\t\ttest_msg(\"      i: %\"PRIuMAX, \\\n-\t\t\t\t\t\t (uintmax_t)i); \\\n-\t\t\t\t\tbreak; \\\n-\t\t\t\t} \\\n-\t\t\t} \\\n-\t\t} \\\n-\t} while (0)\n-\n-int cmd_main(int argc, const char **argv)\n-{\n-\tif_test (\"static initialization\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"dynamic initialization\") {\n-\t\tstruct strvec vec;\n-\t\tstrvec_init(&vec);\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"clear\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_push(&vec, \"foo\");\n-\t\tstrvec_clear(&vec);\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\t}\n-\n-\tif_test (\"push\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\n-\t\tstrvec_push(&vec, \"foo\");\n-\t\tcheck_strvec(&vec, \"foo\", NULL);\n-\n-\t\tstrvec_push(&vec, \"bar\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushf\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushf(&vec, \"foo: %d\", 1);\n-\t\tcheck_strvec(&vec, \"foo: 1\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushl\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pushv\") {\n-\t\tconst char *strings[] = {\n-\t\t\t\"foo\", \"bar\", \"baz\", NULL,\n-\t\t};\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\n-\t\tstrvec_pushv(&vec, strings);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace at head\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 0, \"replaced\");\n-\t\tcheck_strvec(&vec, \"replaced\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace at tail\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 2, \"replaced\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"replaced\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace in between\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_replace(&vec, 1, \"replaced\");\n-\t\tcheck_strvec(&vec, \"foo\", \"replaced\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"replace with substring\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", NULL);\n-\t\tstrvec_replace(&vec, 0, vec.v[0] + 1);\n-\t\tcheck_strvec(&vec, \"oo\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove at head\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 0);\n-\t\tcheck_strvec(&vec, \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove at tail\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 2);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"remove in between\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_remove(&vec, 1);\n-\t\tcheck_strvec(&vec, \"foo\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pop with empty array\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pop(&vec);\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"pop with non-empty array\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_pushl(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_pop(&vec);\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split empty string\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"\");\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split single item\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo\");\n-\t\tcheck_strvec(&vec, \"foo\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split multiple items\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo bar baz\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", \"baz\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split whitespace only\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \" \\t\\n\");\n-\t\tcheck_strvec(&vec, NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"split multiple consecutive whitespaces\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tstrvec_split(&vec, \"foo\\n\\t bar\");\n-\t\tcheck_strvec(&vec, \"foo\", \"bar\", NULL);\n-\t\tstrvec_clear(&vec);\n-\t}\n-\n-\tif_test (\"detach\") {\n-\t\tstruct strvec vec = STRVEC_INIT;\n-\t\tconst char **detached;\n-\n-\t\tstrvec_push(&vec, \"foo\");\n-\n-\t\tdetached = strvec_detach(&vec);\n-\t\tcheck_str(detached[0], \"foo\");\n-\t\tcheck_pointer_eq(detached[1], NULL);\n-\n-\t\tcheck_pointer_eq(vec.v, empty_strvec);\n-\t\tcheck_uint(vec.nr, ==, 0);\n-\t\tcheck_uint(vec.alloc, ==, 0);\n-\n-\t\tfree((char *) detached[0]);\n-\t\tfree(detached);\n-\t}\n-\n-\treturn test_done();\n-}\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502130","messageId":"ba05b9f1eef8136e087846ee54a076558097a240.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 13/14] t/unit-tests: convert ctype tests to use clar","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:17:22Z","receivedAt":"2024-09-04T14:17:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Convert the ctype tests to use the new clar unit testing framework.\nIntroduce a new function `cl_failf()` that allows us to print a\nformatted error message, which we can use to point out which of the\ncharacters was classified incorrectly. This results in output like this\non failure:\n\n    # start of suite 1: ctype\n    not ok 1 - ctype::isspace\n        ---\n        reason: |\n          Test failed.\n          0x0d is classified incorrectly: expected 0, got 1\n        at:\n          file: 't/unit-tests/ctype.c'\n          line: 36\n          function: 'test_ctype__isspace'\n        ---\n    ok 2 - ctype::isdigit\n    ok 3 - ctype::isalpha\n    ok 4 - ctype::isalnum\n    ok 5 - ctype::is_glob_special\n    ok 6 - ctype::is_regex_special\n    ok 7 - ctype::is_pathspec_magic\n    ok 8 - ctype::isascii\n    ok 9 - ctype::islower\n    ok 10 - ctype::isupper\n    ok 11 - ctype::iscntrl\n    ok 12 - ctype::ispunct\n    ok 13 - ctype::isxdigit\n    ok 14 - ctype::isprint\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile                            |  2 +-\n t/unit-tests/{t-ctype.c => ctype.c} | 71 +++++++++++++++++++++++++----\n t/unit-tests/unit-test.h            |  7 +++\n 3 files changed, 69 insertions(+), 11 deletions(-)\n rename t/unit-tests/{t-ctype.c => ctype.c} (68%)\n\ndiff --git a/Makefile b/Makefile\nindex 56ce6c00e44..c841cf70063 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,13 +1336,13 @@ THIRD_PARTY_SOURCES += sha1dc/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/%\n THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/%\n \n+UNIT_TESTS_SUITES += ctype\n UNIT_TESTS_SUITES += strvec\n UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n \n-UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n UNIT_TEST_PROGRAMS += t-hashmap\ndiff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/ctype.c\nsimilarity index 68%\nrename from t/unit-tests/t-ctype.c\nrename to t/unit-tests/ctype.c\nindex e28a7f50f9a..32e65867cdc 100644\n--- a/t/unit-tests/t-ctype.c\n+++ b/t/unit-tests/ctype.c\n@@ -1,16 +1,16 @@\n-#include \"test-lib.h\"\n+#include \"unit-test.h\"\n \n #define TEST_CHAR_CLASS(class, string) do { \\\n \tsize_t len = ARRAY_SIZE(string) - 1 + \\\n \t\tBUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \\\n \t\tBUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \\\n-\tif_test (#class \" works\") { \\\n-\t\tfor (int i = 0; i < 256; i++) { \\\n-\t\t\tif (!check_int(class(i), ==, !!memchr(string, i, len)))\\\n-\t\t\t\ttest_msg(\"      i: 0x%02x\", i); \\\n-\t\t} \\\n-\t\tcheck(!class(EOF)); \\\n+\tfor (int i = 0; i < 256; i++) { \\\n+\t\tint actual = class(i), expect = !!memchr(string, i, len); \\\n+\t\tif (actual != expect) \\\n+\t\t\tcl_failf(\"0x%02x is classified incorrectly: expected %d, got %d\", \\\n+\t\t\t\t i, expect, actual); \\\n \t} \\\n+\tcl_assert(!class(EOF)); \\\n } while (0)\n \n #define DIGIT \"0123456789\"\n@@ -31,21 +31,72 @@\n \t\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\" \\\n \t\"\\x7f\"\n \n-int cmd_main(int argc, const char **argv) {\n+void test_ctype__isspace(void)\n+{\n \tTEST_CHAR_CLASS(isspace, \" \\n\\r\\t\");\n+}\n+\n+void test_ctype__isdigit(void)\n+{\n \tTEST_CHAR_CLASS(isdigit, DIGIT);\n+}\n+\n+void test_ctype__isalpha(void)\n+{\n \tTEST_CHAR_CLASS(isalpha, LOWER UPPER);\n+}\n+\n+void test_ctype__isalnum(void)\n+{\n \tTEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);\n+}\n+\n+void test_ctype__is_glob_special(void)\n+{\n \tTEST_CHAR_CLASS(is_glob_special, \"*?[\\\\\");\n+}\n+\n+void test_ctype__is_regex_special(void)\n+{\n \tTEST_CHAR_CLASS(is_regex_special, \"$()*+.?[\\\\^{|\");\n+}\n+\n+void test_ctype__is_pathspec_magic(void)\n+{\n \tTEST_CHAR_CLASS(is_pathspec_magic, \"!\\\"#%&',-/:;<=>@_`~\");\n+}\n+\n+void test_ctype__isascii(void)\n+{\n \tTEST_CHAR_CLASS(isascii, ASCII);\n+}\n+\n+void test_ctype__islower(void)\n+{\n \tTEST_CHAR_CLASS(islower, LOWER);\n+}\n+\n+void test_ctype__isupper(void)\n+{\n \tTEST_CHAR_CLASS(isupper, UPPER);\n+}\n+\n+void test_ctype__iscntrl(void)\n+{\n \tTEST_CHAR_CLASS(iscntrl, CNTRL);\n+}\n+\n+void test_ctype__ispunct(void)\n+{\n \tTEST_CHAR_CLASS(ispunct, PUNCT);\n+}\n+\n+void test_ctype__isxdigit(void)\n+{\n \tTEST_CHAR_CLASS(isxdigit, DIGIT \"abcdefABCDEF\");\n-\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n+}\n \n-\treturn test_done();\n+void test_ctype__isprint(void)\n+{\n+\tTEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT \" \");\n }\ndiff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h\nindex 66ec2387cc6..85e5d6a948a 100644\n--- a/t/unit-tests/unit-test.h\n+++ b/t/unit-tests/unit-test.h\n@@ -1,3 +1,10 @@\n #include \"git-compat-util.h\"\n #include \"clar/clar.h\"\n #include \"clar-decls.h\"\n+#include \"strbuf.h\"\n+\n+#define cl_failf(fmt, ...) do { \\\n+\tchar desc[4096]; \\\n+\tsnprintf(desc, sizeof(desc), fmt, __VA_ARGS__); \\\n+\tclar__fail(__FILE__, __func__, __LINE__, \"Test failed.\", desc, 1); \\\n+} while (0)\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502131","messageId":"8441d29daa868f74a55b7c1111edb336c5502ebc.1725459142.git.ps@pks.im","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"[PATCH v8 14/14] clar: add CMake support","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-04T14:17:25Z","receivedAt":"2024-09-04T14:17:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nNow that we're using `clar` as powerful test framework, we have to\nadjust the Visual C build (read: the CMake definition) to be able to\nhandle that, too.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n contrib/buildsystems/CMakeLists.txt | 53 +++++++++++++++++++++++++++++\n 1 file changed, 53 insertions(+)\n\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 832f46b316b..608fd3fe709 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -1004,6 +1004,59 @@ foreach(unit_test ${unit_test_PROGRAMS})\n \tendif()\n endforeach()\n \n+parse_makefile_for_scripts(unit_tests_SUITES \"UNIT_TESTS_SUITES\" \"\")\n+\n+set(clar_decls \"\")\n+set(clar_cbs \"\")\n+set(clar_cbs_count 0)\n+set(clar_suites \"static struct clar_suite _clar_suites[] = {\\n\")\n+list(LENGTH unit_tests_SUITES clar_suites_count)\n+foreach(suite ${unit_tests_SUITES})\n+\tfile(STRINGS \"${CMAKE_SOURCE_DIR}/t/unit-tests/${suite}.c\" decls\n+\t\tREGEX \"^void test_${suite}__[a-zA-Z_0-9][a-zA-Z_0-9]*\\\\(void\\\\)$\")\n+\n+\tlist(LENGTH decls decls_count)\n+\tstring(REGEX REPLACE \"void (test_${suite}__([a-zA-Z_0-9]*))\\\\(void\\\\)\" \"    { \\\"\\\\2\\\", &\\\\1 },\\n\" cbs ${decls})\n+\tstring(JOIN \"\" cbs ${cbs})\n+\tlist(TRANSFORM decls PREPEND \"extern \")\n+\tstring(JOIN \";\\n\" decls ${decls})\n+\n+\tstring(APPEND clar_decls \"${decls};\\n\")\n+\tstring(APPEND clar_cbs\n+\t\t\"static const struct clar_func _clar_cb_${suite}[] = {\\n\"\n+\t\t${cbs}\n+\t\t\"};\\n\")\n+\tstring(APPEND clar_suites\n+\t\t\"    {\\n\"\n+\t\t\"        \\\"${suite}\\\",\\n\"\n+\t\t\"        { NULL, NULL },\\n\"\n+\t\t\"        { NULL, NULL },\\n\"\n+\t\t\"        _clar_cb_${suite}, ${decls_count}, 1\\n\"\n+\t\t\"    },\\n\")\n+\tmath(EXPR clar_cbs_count \"${clar_cbs_count}+${decls_count}\")\n+endforeach()\n+string(APPEND clar_suites\n+\t\"};\\n\"\n+\t\"static const size_t _clar_suite_count = ${clar_suites_count};\\n\"\n+\t\"static const size_t _clar_callback_count = ${clar_cbs_count};\\n\")\n+file(WRITE \"${CMAKE_BINARY_DIR}/t/unit-tests/clar-decls.h\" \"${clar_decls}\")\n+file(WRITE \"${CMAKE_BINARY_DIR}/t/unit-tests/clar.suite\" \"${clar_decls}\" \"${clar_cbs}\" \"${clar_suites}\")\n+\n+list(TRANSFORM unit_tests_SUITES PREPEND \"${CMAKE_SOURCE_DIR}/t/unit-tests/\")\n+list(TRANSFORM unit_tests_SUITES APPEND \".c\")\n+add_library(unit-tests-lib ${unit_tests_SUITES} \"${CMAKE_SOURCE_DIR}/t/unit-tests/clar/clar.c\")\n+target_include_directories(unit-tests-lib PRIVATE \"${CMAKE_SOURCE_DIR}/t/unit-tests\")\n+add_executable(unit-tests \"${CMAKE_SOURCE_DIR}/t/unit-tests/unit-test.c\")\n+target_link_libraries(unit-tests unit-tests-lib common-main)\n+set_target_properties(unit-tests\n+\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+if(MSVC)\n+\tset_target_properties(unit-tests\n+\t\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+\tset_target_properties(unit-tests\n+\t\tPROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/t/unit-tests/bin)\n+endif()\n+\n #test-tool\n parse_makefile_for_sources(test-tool_SOURCES \"TEST_BUILTINS_OBJS\")\n add_library(test-lib OBJECT ${CMAKE_SOURCE_DIR}/t/unit-tests/test-lib.c)\n-- \n2.46.0.519.g2e7b89e038.dirty\n\n"},{"id":"502132","messageId":"d5b1c95b-cbdc-4711-849e-c2cfc67787ee@gmail.com","threadId":"61874","inReplyTo":"cover.1725459142.git.ps@pks.im","subject":"Re: [PATCH v8 00/14] Introduce clar testing framework","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-09-04T14:32:53Z","receivedAt":"2024-09-04T14:32:57Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 04/09/2024 15:16, Patrick Steinhardt wrote:\n> \n> Changes compared to v7:\n> \n>    - Properly wire up the \"--immediate\" flag.\n> \n>    - Give a hint for the syntax to run only specific suites or tests for\n>      the \"--run\" and \"--exclude\" options.\n\nAs far as I'm concerned this version looks ready to be merged to next\n\nThanks\n\nPhillip\n\n> Thanks!\n> \n> Patrick\n> \n> Johannes Schindelin (4):\n>    clar: avoid compile error with mingw-w64\n>    clar(win32): avoid compile error due to unused `fs_copy()`\n>    clar: stop including `shellapi.h` unnecessarily\n>    clar: add CMake support\n> \n> Patrick Steinhardt (10):\n>    t: do not pass GIT_TEST_OPTS to unit tests with prove\n>    t: import the clar unit testing framework\n>    t/clar: fix compatibility with NonStop\n>    Makefile: fix sparse dependency on GENERATED_H\n>    Makefile: make hdr-check depend on generated headers\n>    Makefile: do not use sparse on third-party sources\n>    Makefile: wire up the clar unit testing framework\n>    t/unit-tests: implement test driver\n>    t/unit-tests: convert strvec tests to use clar\n>    t/unit-tests: convert ctype tests to use clar\n> \n>   .gitignore                                 |   1 +\n>   Documentation/technical/unit-tests.txt     |   2 +\n>   Makefile                                   |  53 +-\n>   contrib/buildsystems/CMakeLists.txt        |  53 ++\n>   t/Makefile                                 |   4 +-\n>   t/run-test.sh                              |   2 +-\n>   t/unit-tests/.gitignore                    |   2 +\n>   t/unit-tests/clar-generate.awk             |  50 ++\n>   t/unit-tests/clar/.github/workflows/ci.yml |  23 +\n>   t/unit-tests/clar/COPYING                  |  15 +\n>   t/unit-tests/clar/README.md                | 329 ++++++++\n>   t/unit-tests/clar/clar.c                   | 842 +++++++++++++++++++++\n>   t/unit-tests/clar/clar.h                   | 173 +++++\n>   t/unit-tests/clar/clar/fixtures.h          |  50 ++\n>   t/unit-tests/clar/clar/fs.h                | 524 +++++++++++++\n>   t/unit-tests/clar/clar/print.h             | 211 ++++++\n>   t/unit-tests/clar/clar/sandbox.h           | 159 ++++\n>   t/unit-tests/clar/clar/summary.h           | 143 ++++\n>   t/unit-tests/clar/generate.py              | 266 +++++++\n>   t/unit-tests/clar/test/.gitignore          |   4 +\n>   t/unit-tests/clar/test/Makefile            |  39 +\n>   t/unit-tests/clar/test/clar_test.h         |  16 +\n>   t/unit-tests/clar/test/main.c              |  40 +\n>   t/unit-tests/clar/test/main.c.sample       |  27 +\n>   t/unit-tests/clar/test/resources/test/file |   1 +\n>   t/unit-tests/clar/test/sample.c            |  84 ++\n>   t/unit-tests/{t-ctype.c => ctype.c}        |  71 +-\n>   t/unit-tests/strvec.c                      | 241 ++++++\n>   t/unit-tests/t-strvec.c                    | 211 ------\n>   t/unit-tests/unit-test.c                   |  47 ++\n>   t/unit-tests/unit-test.h                   |  10 +\n>   31 files changed, 3459 insertions(+), 234 deletions(-)\n>   create mode 100644 t/unit-tests/clar-generate.awk\n>   create mode 100644 t/unit-tests/clar/.github/workflows/ci.yml\n>   create mode 100644 t/unit-tests/clar/COPYING\n>   create mode 100644 t/unit-tests/clar/README.md\n>   create mode 100644 t/unit-tests/clar/clar.c\n>   create mode 100644 t/unit-tests/clar/clar.h\n>   create mode 100644 t/unit-tests/clar/clar/fixtures.h\n>   create mode 100644 t/unit-tests/clar/clar/fs.h\n>   create mode 100644 t/unit-tests/clar/clar/print.h\n>   create mode 100644 t/unit-tests/clar/clar/sandbox.h\n>   create mode 100644 t/unit-tests/clar/clar/summary.h\n>   create mode 100755 t/unit-tests/clar/generate.py\n>   create mode 100644 t/unit-tests/clar/test/.gitignore\n>   create mode 100644 t/unit-tests/clar/test/Makefile\n>   create mode 100644 t/unit-tests/clar/test/clar_test.h\n>   create mode 100644 t/unit-tests/clar/test/main.c\n>   create mode 100644 t/unit-tests/clar/test/main.c.sample\n>   create mode 100644 t/unit-tests/clar/test/resources/test/file\n>   create mode 100644 t/unit-tests/clar/test/sample.c\n>   rename t/unit-tests/{t-ctype.c => ctype.c} (68%)\n>   create mode 100644 t/unit-tests/strvec.c\n>   delete mode 100644 t/unit-tests/t-strvec.c\n>   create mode 100644 t/unit-tests/unit-test.c\n>   create mode 100644 t/unit-tests/unit-test.h\n> \n> Range-diff against v7:\n>   1:  b67f10ec0b0 =  1:  b67f10ec0b0 t: do not pass GIT_TEST_OPTS to unit tests with prove\n>   2:  55a9b46e65f =  2:  55a9b46e65f t: import the clar unit testing framework\n>   3:  f24401f0a87 =  3:  f24401f0a87 t/clar: fix compatibility with NonStop\n>   4:  658a601c541 =  4:  658a601c541 clar: avoid compile error with mingw-w64\n>   5:  0b8a6ac5fed =  5:  0b8a6ac5fed clar(win32): avoid compile error due to unused `fs_copy()`\n>   6:  c50e7a0ea68 =  6:  c50e7a0ea68 clar: stop including `shellapi.h` unnecessarily\n>   7:  b8f3f16dd27 =  7:  b8f3f16dd27 Makefile: fix sparse dependency on GENERATED_H\n>   8:  3d3fe443b9a =  8:  3d3fe443b9a Makefile: make hdr-check depend on generated headers\n>   9:  7d0f494850a =  9:  7d0f494850a Makefile: do not use sparse on third-party sources\n> 10:  9c74c5ae019 = 10:  9c74c5ae019 Makefile: wire up the clar unit testing framework\n> 11:  8bd5b3e2b29 ! 11:  81d932bfa33 t/unit-tests: implement test driver\n>      @@ t/unit-tests/unit-test.c\n>       +\tstruct string_list exclude_args = STRING_LIST_INIT_NODUP;\n>       +\tint immediate = 0;\n>       +\tstruct option options[] = {\n>      -+\t\tOPT_BOOL('i', \"--immediate\", &immediate,\n>      ++\t\tOPT_BOOL('i', \"immediate\", &immediate,\n>       +\t\t\t N_(\"immediately exit upon the first failed test\")),\n>      -+\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"name\"),\n>      -+\t\t\t\tN_(\"run only test suite or individual test <name>\")),\n>      -+\t\tOPT_STRING_LIST('x', \"exclude\", &exclude_args, N_(\"name\"),\n>      -+\t\t\t\tN_(\"exclude test suite <name>\")),\n>      ++\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"suite[::test]\"),\n>      ++\t\t\t\tN_(\"run only test suite or individual test <suite[::test]>\")),\n>      ++\t\tOPT_STRING_LIST('x', \"exclude\", &exclude_args, N_(\"suite\"),\n>      ++\t\t\t\tN_(\"exclude test suite <suite>\")),\n>       +\t\tOPT_END(),\n>       +\t};\n>       +\tstruct strvec args = STRVEC_INIT;\n>      @@ t/unit-tests/unit-test.c\n>       +\n>       +\tstrvec_push(&args, argv[0]);\n>       +\tstrvec_push(&args, \"-t\");\n>      ++\tif (immediate)\n>      ++\t\tstrvec_push(&args, \"-Q\");\n>       +\tfor (size_t i = 0; i < run_args.nr; i++)\n>       +\t\tstrvec_pushf(&args, \"-s%s\", run_args.items[i].string);\n>       +\tfor (size_t i = 0; i < exclude_args.nr; i++)\n> 12:  3c3b9eacdfb = 12:  604303e31aa t/unit-tests: convert strvec tests to use clar\n> 13:  c8360db2f86 = 13:  ba05b9f1eef t/unit-tests: convert ctype tests to use clar\n> 14:  d51c146cd9d = 14:  8441d29daa8 clar: add CMake support\n"},{"id":"502134","messageId":"6e941da7-cf7d-473c-bd32-56bebee6277f@gmail.com","threadId":"61874","inReplyTo":"ZthqtWVntB-PgSH-@pks.im","subject":"Re: [PATCH v7 11/14] t/unit-tests: implement test driver","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-09-04T14:35:35Z","receivedAt":"2024-09-04T14:35:40Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 04/09/2024 15:12, Patrick Steinhardt wrote:\n> On Wed, Sep 04, 2024 at 02:35:20PM +0100, Phillip Wood wrote:\n> Oh, that's a plain oversight on my side. It is easy to wire up given\n> that the clar already supports it via \"-Q\". Also made me notice that I\n> wrote \"--immediate\" instead of \"immediate\".\n\nThat's handy, I'd missed clar's \"-Q\" option.\n\n>>> +\t\tOPT_STRING_LIST('r', \"run\", &run_args, N_(\"name\"),\n>>> +\t\t\t\tN_(\"run only test suite or individual test <name>\")),\n>>\n>> It's nice that this option name now matches our integration tests. It would\n>> be helpful to show the syntax for \"name\" (I think it expects\n>> <suite>[::<test>]) but I failed to come up with a concise description to add\n>> to the help here.\n> \n> Isn't `<suite[::test]>` concise enough? I certainly like it.\n\nSounds good\n\nPhillip\n\n> Patrick\n"},{"id":"502468","messageId":"xmqqfrq8r8s8.fsf@gitster.g","threadId":"61874","inReplyTo":"9c74c5ae01989659c0347d2742f820d2161d274b.1725459142.git.ps@pks.im","subject":"Re: [PATCH v8 10/14] Makefile: wire up the clar unit testing framework","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-09-09T18:17:43Z","receivedAt":"2024-09-09T18:17:46Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> +UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n> +UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n> +UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n> +UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n> +\n>  UNIT_TEST_PROGRAMS += t-ctype\n>  UNIT_TEST_PROGRAMS += t-example-decorate\n>  UNIT_TEST_PROGRAMS += t-hash\n> @@ -2714,6 +2721,7 @@ OBJECTS += $(XDIFF_OBJS)\n>  OBJECTS += $(FUZZ_OBJS)\n>  OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n>  OBJECTS += $(UNIT_TEST_OBJS)\n> +OBJECTS += $(UNIT_TESTS_OBJS)\n\nWhat is the longer term direction we envision?  Do we aim to retire\nthe UNIT_TEST_* and end up with only the clar based tests?\n\nAt least until that happens, can we have \"UNIT_TESTS\" -> \"CLAR_TEST\"\nor something that makes it less confusing?  Every time I see merge\nconflicts around this area in the Makefile, I get puzzled and wonder\nwhich one is which.\n\nThanks.\n\n\n"},{"id":"502536","messageId":"Zt_lLsnylKJ9uoqj@pks.im","threadId":"61874","inReplyTo":"xmqqfrq8r8s8.fsf@gitster.g","subject":"Re: [PATCH v8 10/14] Makefile: wire up the clar unit testing framework","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2024-09-10T06:20:36Z","receivedAt":"2024-09-10T06:20:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 09, 2024 at 11:17:43AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > +UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)\n> > +UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))\n> > +UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o\n> > +UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o\n> > +\n> >  UNIT_TEST_PROGRAMS += t-ctype\n> >  UNIT_TEST_PROGRAMS += t-example-decorate\n> >  UNIT_TEST_PROGRAMS += t-hash\n> > @@ -2714,6 +2721,7 @@ OBJECTS += $(XDIFF_OBJS)\n> >  OBJECTS += $(FUZZ_OBJS)\n> >  OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)\n> >  OBJECTS += $(UNIT_TEST_OBJS)\n> > +OBJECTS += $(UNIT_TESTS_OBJS)\n> \n> What is the longer term direction we envision?  Do we aim to retire\n> the UNIT_TEST_* and end up with only the clar based tests?\n\nYeah, that's my plan. I first want to let the new clar-based tests cook\na bit to shake out any issues with it, and address issues with clar that\nwere raised during review upstream. But once that's done and we see that\nit works alright for our purpose I'd aim to convert all test suites to\nuse the clar such that we ultimately end up with a single unit testing\nframework, only.\n\n> At least until that happens, can we have \"UNIT_TESTS\" -> \"CLAR_TEST\"\n> or something that makes it less confusing?  Every time I see merge\n> conflicts around this area in the Makefile, I get puzzled and wonder\n> which one is which.\n\nThese are indeed quite similarly named, so I see where the confusion\ncomes from. I'll send a patch in a bit to rename them.\n\nPatrick\n"}]}