aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-21 12:20:34 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-21 12:20:34 +0100
commit8a36eb498e1a1c2cf2e886356faa4ce67e52e874 (patch)
tree92e44b4d4ddef68ff91d35f44ca57a9d45e7f879 /tests
downloadnihil-8a36eb498e1a1c2cf2e886356faa4ce67e52e874.tar.gz
nihil-8a36eb498e1a1c2cf2e886356faa4ce67e52e874.tar.bz2
initial commit
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt21
-rw-r--r--tests/ctype.cc373
-rw-r--r--tests/fd.cc200
-rw-r--r--tests/generator.cc56
-rw-r--r--tests/generic_error.cc17
-rw-r--r--tests/getenv.cc48
-rw-r--r--tests/guard.cc20
-rw-r--r--tests/tabulate.cc75
8 files changed, 810 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 0000000..c6788a3
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,21 @@
+# This source code is released into the public domain.
+
+add_executable(nihil.test
+ ctype.cc
+ fd.cc
+ generator.cc
+ generic_error.cc
+ getenv.cc
+ guard.cc
+ tabulate.cc)
+
+target_link_libraries(nihil.test PRIVATE
+ nihil
+ Catch2::Catch2WithMain
+)
+
+find_package(Catch2 REQUIRED)
+
+include(CTest)
+include(Catch)
+catch_discover_tests(nihil.test)
diff --git a/tests/ctype.cc b/tests/ctype.cc
new file mode 100644
index 0000000..87f5103
--- /dev/null
+++ b/tests/ctype.cc
@@ -0,0 +1,373 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("ctype: space", "[ctype]") {
+ auto is_utf8_space =
+ nihil::ctype_is(std::ctype_base::space,
+ std::locale("C.UTF-8"));
+
+ // '\v' (vertical tab) is a space
+ REQUIRE(nihil::is_space('\v') == true);
+ REQUIRE(nihil::is_space(L'\v') == true);
+
+ REQUIRE(nihil::is_c_space('\v') == true);
+ REQUIRE(nihil::is_c_space(L'\v') == true);
+
+ REQUIRE(is_utf8_space('\v') == true);
+ REQUIRE(is_utf8_space(L'\v') == true);
+
+ // 'x' is not a space
+ REQUIRE(nihil::is_space('x') == false);
+ REQUIRE(nihil::is_space(L'x') == false);
+
+ REQUIRE(nihil::is_c_space('x') == false);
+ REQUIRE(nihil::is_c_space(L'x') == false);
+
+ REQUIRE(is_utf8_space('x') == false);
+ REQUIRE(is_utf8_space(L'x') == false);
+
+ // U+2003 EM SPACE is a space
+ REQUIRE(nihil::is_space(L'\u2003') == false);
+ REQUIRE(nihil::is_c_space(L'\u2003') == false);
+ REQUIRE(is_utf8_space(L'\u2003') == true);
+}
+
+TEST_CASE("ctype: print", "[ctype]") {
+ auto is_utf8_print =
+ nihil::ctype_is(std::ctype_base::print,
+ std::locale("C.UTF-8"));
+
+ // 'x' is printable
+ REQUIRE(nihil::is_print('x') == true);
+ REQUIRE(nihil::is_print(L'x') == true);
+
+ REQUIRE(nihil::is_c_print('x') == true);
+ REQUIRE(nihil::is_c_print(L'x') == true);
+
+ REQUIRE(is_utf8_print('x') == true);
+ REQUIRE(is_utf8_print(L'x') == true);
+
+ // '\003' is not printable
+ REQUIRE(nihil::is_print('\003') == false);
+ REQUIRE(nihil::is_print(L'\003') == false);
+
+ REQUIRE(nihil::is_c_print('\003') == false);
+ REQUIRE(nihil::is_c_print(L'\003') == false);
+
+ REQUIRE(is_utf8_print('\003') == false);
+ REQUIRE(is_utf8_print(L'\003') == false);
+
+ // U+0410 CYRILLIC CAPITAL LETTER A is printable
+ REQUIRE(nihil::is_print(L'\u0410') == false);
+ REQUIRE(nihil::is_c_print(L'\u0410') == false);
+ REQUIRE(is_utf8_print(L'\u0410') == true);
+}
+
+TEST_CASE("ctype: cntrl", "[ctype]") {
+ auto is_utf8_cntrl =
+ nihil::ctype_is(std::ctype_base::cntrl,
+ std::locale("C.UTF-8"));
+
+ // '\003' is a control character
+ REQUIRE(nihil::is_cntrl('\003') == true);
+ REQUIRE(nihil::is_cntrl(L'\003') == true);
+
+ REQUIRE(nihil::is_c_cntrl('\003') == true);
+ REQUIRE(nihil::is_c_cntrl(L'\003') == true);
+
+ REQUIRE(is_utf8_cntrl('\003') == true);
+ REQUIRE(is_utf8_cntrl(L'\003') == true);
+
+
+ // 'x' is not a control character
+ REQUIRE(nihil::is_cntrl('x') == false);
+ REQUIRE(nihil::is_cntrl(L'x') == false);
+
+ REQUIRE(nihil::is_c_cntrl('x') == false);
+ REQUIRE(nihil::is_c_cntrl(L'x') == false);
+
+ REQUIRE(is_utf8_cntrl('x') == false);
+ REQUIRE(is_utf8_cntrl(L'x') == false);
+
+ // U+00AD SOFT HYPHEN is a control character.
+ REQUIRE(nihil::is_cntrl(L'\u00ad') == false);
+ REQUIRE(nihil::is_c_cntrl(L'\u00ad') == false);
+ REQUIRE(is_utf8_cntrl(L'\u00ad') == true);
+}
+
+TEST_CASE("ctype: upper", "[ctype]") {
+ auto is_utf8_upper =
+ nihil::ctype_is(std::ctype_base::upper,
+ std::locale("C.UTF-8"));
+
+ // 'A' is upper case
+ REQUIRE(nihil::is_upper('A') == true);
+ REQUIRE(nihil::is_upper(L'A') == true);
+
+ REQUIRE(nihil::is_c_upper('A') == true);
+ REQUIRE(nihil::is_c_upper(L'A') == true);
+
+ REQUIRE(is_utf8_upper('A') == true);
+ REQUIRE(is_utf8_upper(L'A') == true);
+
+ // 'a' is not upper case
+ REQUIRE(nihil::is_upper('a') == false);
+ REQUIRE(nihil::is_upper(L'a') == false);
+
+ REQUIRE(nihil::is_c_upper('a') == false);
+ REQUIRE(nihil::is_c_upper(L'a') == false);
+
+ REQUIRE(is_utf8_upper('a') == false);
+ REQUIRE(is_utf8_upper(L'a') == false);
+
+ // U+0410 CYRILLIC CAPITAL LETTER A is upper case
+ REQUIRE(nihil::is_upper(L'\u0410') == false);
+ REQUIRE(nihil::is_c_upper(L'\u0410') == false);
+ REQUIRE(is_utf8_upper(L'\u0410') == true);
+}
+
+TEST_CASE("ctype: lower", "[ctype]") {
+ auto is_utf8_lower =
+ nihil::ctype_is(std::ctype_base::lower,
+ std::locale("C.UTF-8"));
+
+ // 'a' is lower case
+ REQUIRE(nihil::is_lower('a') == true);
+ REQUIRE(nihil::is_lower(L'a') == true);
+
+ REQUIRE(nihil::is_c_lower('a') == true);
+ REQUIRE(nihil::is_c_lower(L'a') == true);
+
+ REQUIRE(is_utf8_lower('a') == true);
+ REQUIRE(is_utf8_lower(L'a') == true);
+
+ // 'A' is not lower case
+ REQUIRE(nihil::is_lower('A') == false);
+ REQUIRE(nihil::is_lower(L'A') == false);
+
+ REQUIRE(nihil::is_c_lower('A') == false);
+ REQUIRE(nihil::is_c_lower(L'A') == false);
+
+ REQUIRE(is_utf8_lower('A') == false);
+ REQUIRE(is_utf8_lower(L'A') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_lower(L'\u0430') == false);
+ REQUIRE(nihil::is_c_lower(L'\u0430') == false);
+ REQUIRE(is_utf8_lower(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: alpha", "[ctype]") {
+ auto is_utf8_alpha =
+ nihil::ctype_is(std::ctype_base::alpha,
+ std::locale("C.UTF-8"));
+
+ // 'a' is alphabetical
+ REQUIRE(nihil::is_alpha('a') == true);
+ REQUIRE(nihil::is_alpha(L'a') == true);
+
+ REQUIRE(nihil::is_c_alpha('a') == true);
+ REQUIRE(nihil::is_c_alpha(L'a') == true);
+
+ REQUIRE(is_utf8_alpha('a') == true);
+ REQUIRE(is_utf8_alpha(L'a') == true);
+
+ // '1' is not alphabetical
+ REQUIRE(nihil::is_alpha('1') == false);
+ REQUIRE(nihil::is_alpha(L'1') == false);
+
+ REQUIRE(nihil::is_c_alpha('1') == false);
+ REQUIRE(nihil::is_c_alpha(L'1') == false);
+
+ REQUIRE(is_utf8_alpha('1') == false);
+ REQUIRE(is_utf8_alpha(L'1') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_alpha(L'\u0430') == false);
+ REQUIRE(nihil::is_c_alpha(L'\u0430') == false);
+ REQUIRE(is_utf8_alpha(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: digit", "[ctype]") {
+ auto is_utf8_digit =
+ nihil::ctype_is(std::ctype_base::digit,
+ std::locale("C.UTF-8"));
+
+ // '1' is a digit
+ REQUIRE(nihil::is_digit('1') == true);
+ REQUIRE(nihil::is_digit(L'1') == true);
+
+ REQUIRE(nihil::is_c_digit('1') == true);
+ REQUIRE(nihil::is_c_digit(L'1') == true);
+
+ REQUIRE(is_utf8_digit('1') == true);
+ REQUIRE(is_utf8_digit(L'1') == true);
+
+ // 'a' is not a digit
+ REQUIRE(nihil::is_digit('a') == false);
+ REQUIRE(nihil::is_digit(L'a') == false);
+
+ REQUIRE(nihil::is_c_digit('a') == false);
+ REQUIRE(nihil::is_c_digit(L'a') == false);
+
+ REQUIRE(is_utf8_digit('a') == false);
+ REQUIRE(is_utf8_digit(L'a') == false);
+
+ // U+0660 ARABIC-INDIC DIGIT ZERO
+ REQUIRE(nihil::is_digit(L'\u0660') == false);
+ REQUIRE(nihil::is_c_digit(L'\u0660') == false);
+ REQUIRE(is_utf8_digit(L'\u0660') == true);
+}
+
+TEST_CASE("ctype: punct", "[ctype]") {
+ auto is_utf8_punct =
+ nihil::ctype_is(std::ctype_base::punct,
+ std::locale("C.UTF-8"));
+
+ // ';' is punctuation
+ REQUIRE(nihil::is_punct(';') == true);
+ REQUIRE(nihil::is_punct(L';') == true);
+
+ REQUIRE(nihil::is_c_punct(';') == true);
+ REQUIRE(nihil::is_c_punct(L';') == true);
+
+ REQUIRE(is_utf8_punct(';') == true);
+ REQUIRE(is_utf8_punct(L';') == true);
+
+ // 'a' is not punctuation
+ REQUIRE(nihil::is_punct('a') == false);
+ REQUIRE(nihil::is_punct(L'a') == false);
+
+ REQUIRE(nihil::is_c_punct('a') == false);
+ REQUIRE(nihil::is_c_punct(L'a') == false);
+
+ REQUIRE(is_utf8_punct('a') == false);
+ REQUIRE(is_utf8_punct(L'a') == false);
+
+ // U+00A1 INVERTED EXCLAMATION MARK
+ REQUIRE(nihil::is_punct(L'\u00A1') == false);
+ REQUIRE(nihil::is_c_punct(L'\u00A1') == false);
+ REQUIRE(is_utf8_punct(L'\u00A1') == true);
+}
+
+TEST_CASE("ctype: xdigit", "[ctype]") {
+ auto is_utf8_xdigit =
+ nihil::ctype_is(std::ctype_base::xdigit,
+ std::locale("C.UTF-8"));
+
+ // 'f' is an xdigit
+ REQUIRE(nihil::is_xdigit('f') == true);
+ REQUIRE(nihil::is_xdigit(L'f') == true);
+
+ REQUIRE(nihil::is_c_xdigit('f') == true);
+ REQUIRE(nihil::is_c_xdigit(L'f') == true);
+
+ REQUIRE(is_utf8_xdigit('f') == true);
+ REQUIRE(is_utf8_xdigit(L'f') == true);
+
+ // 'g' is not an xdigit
+ REQUIRE(nihil::is_xdigit('g') == false);
+ REQUIRE(nihil::is_xdigit(L'g') == false);
+
+ REQUIRE(nihil::is_c_xdigit('g') == false);
+ REQUIRE(nihil::is_c_xdigit(L'g') == false);
+
+ REQUIRE(is_utf8_xdigit('g') == false);
+ REQUIRE(is_utf8_xdigit(L'g') == false);
+}
+
+TEST_CASE("ctype: blank", "[ctype]") {
+ auto is_utf8_blank =
+ nihil::ctype_is(std::ctype_base::blank,
+ std::locale("C.UTF-8"));
+
+ // '\t' is a blank
+ REQUIRE(nihil::is_blank('\t') == true);
+ REQUIRE(nihil::is_blank(L'\t') == true);
+
+ REQUIRE(nihil::is_c_blank('\t') == true);
+ REQUIRE(nihil::is_c_blank(L'\t') == true);
+
+ REQUIRE(is_utf8_blank('\t') == true);
+ REQUIRE(is_utf8_blank(L'\t') == true);
+
+ // '\v' is not a blank
+ REQUIRE(nihil::is_blank('\v') == false);
+ REQUIRE(nihil::is_blank(L'\v') == false);
+
+ REQUIRE(nihil::is_c_blank('\v') == false);
+ REQUIRE(nihil::is_c_blank(L'\v') == false);
+
+ REQUIRE(is_utf8_blank('\v') == false);
+ REQUIRE(is_utf8_blank(L'\v') == false);
+
+ // There don't seem to be any UTF-8 blank characters, at least
+ // in FreeBSD libc.
+}
+
+TEST_CASE("ctype: alnum", "[ctype]") {
+ auto is_utf8_alnum =
+ nihil::ctype_is(std::ctype_base::alnum,
+ std::locale("C.UTF-8"));
+
+ // 'a' is alphanumeric
+ REQUIRE(nihil::is_alnum('a') == true);
+ REQUIRE(nihil::is_alnum(L'a') == true);
+
+ REQUIRE(nihil::is_c_alnum('a') == true);
+ REQUIRE(nihil::is_c_alnum(L'a') == true);
+
+ REQUIRE(is_utf8_alnum('a') == true);
+ REQUIRE(is_utf8_alnum(L'a') == true);
+
+ // '\t' is not a alnum
+ REQUIRE(nihil::is_alnum('\t') == false);
+ REQUIRE(nihil::is_alnum(L'\t') == false);
+
+ REQUIRE(nihil::is_c_alnum('\t') == false);
+ REQUIRE(nihil::is_c_alnum(L'\t') == false);
+
+ REQUIRE(is_utf8_alnum('\t') == false);
+ REQUIRE(is_utf8_alnum(L'\t') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_alnum(L'\u0430') == false);
+ REQUIRE(nihil::is_c_alnum(L'\u0430') == false);
+ REQUIRE(is_utf8_alnum(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: graph", "[ctype]") {
+ auto is_utf8_graph =
+ nihil::ctype_is(std::ctype_base::graph,
+ std::locale("C.UTF-8"));
+
+ // 'a' is graphical
+ REQUIRE(nihil::is_graph('a') == true);
+ REQUIRE(nihil::is_graph(L'a') == true);
+
+ REQUIRE(nihil::is_c_graph('a') == true);
+ REQUIRE(nihil::is_c_graph(L'a') == true);
+
+ REQUIRE(is_utf8_graph('a') == true);
+ REQUIRE(is_utf8_graph(L'a') == true);
+
+ // '\t' is not graphical
+ REQUIRE(nihil::is_graph('\t') == false);
+ REQUIRE(nihil::is_graph(L'\t') == false);
+
+ REQUIRE(nihil::is_c_graph('\t') == false);
+ REQUIRE(nihil::is_c_graph(L'\t') == false);
+
+ REQUIRE(is_utf8_graph('\t') == false);
+ REQUIRE(is_utf8_graph(L'\t') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_graph(L'\u0430') == false);
+ REQUIRE(nihil::is_c_graph(L'\u0430') == false);
+ REQUIRE(is_utf8_graph(L'\u0430') == true);
+}
diff --git a/tests/fd.cc b/tests/fd.cc
new file mode 100644
index 0000000..fbf353e
--- /dev/null
+++ b/tests/fd.cc
@@ -0,0 +1,200 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <span>
+
+#include <stdio.h>
+#include <fcntl.h>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+using namespace std::literals;
+
+namespace {
+
+// Test if an fd is open.
+auto fd_is_open(int fd) -> bool {
+ auto const ret = ::fcntl(fd, F_GETFL);
+ return ret == 0;
+}
+
+} // anonymous namespace
+
+TEST_CASE("fd: construct empty", "[fd]") {
+ nihil::fd fd;
+
+ REQUIRE(!fd);
+ REQUIRE_THROWS_AS(fd.get(), nihil::fd_logic_error);
+}
+
+TEST_CASE("fd: construct from fd", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ {
+ auto fd = nihil::fd(file);
+ REQUIRE(fd_is_open(fd.get()));
+ }
+
+ REQUIRE(!fd_is_open(file));
+}
+
+TEST_CASE("fd: close", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ REQUIRE(fd);
+
+ auto const ret = fd.close();
+ REQUIRE(ret);
+ REQUIRE(!fd_is_open(file));
+}
+
+TEST_CASE("fd: move construct", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd1 = nihil::fd(file);
+ REQUIRE(fd_is_open(fd1.get()));
+
+ auto fd2(std::move(fd1));
+ REQUIRE(!fd1);
+ REQUIRE(fd2);
+ REQUIRE(fd2.get() == file);
+ REQUIRE(fd_is_open(file));
+}
+
+TEST_CASE("fd: move assign", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd1 = nihil::fd(file);
+ REQUIRE(fd_is_open(fd1.get()));
+
+ auto fd2 = nihil::fd();
+ REQUIRE(!fd2);
+
+ fd2 = std::move(fd1);
+
+ REQUIRE(!fd1);
+ REQUIRE(fd2);
+ REQUIRE(fd2.get() == file);
+ REQUIRE(fd_is_open(file));
+}
+
+TEST_CASE("fd: release", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ auto fdesc = std::move(fd).release();
+ REQUIRE(!fd);
+ REQUIRE(fdesc == file);
+}
+
+TEST_CASE("fd: dup", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ REQUIRE(fd);
+
+ auto fd2 = dup(fd);
+ REQUIRE(fd2);
+ REQUIRE(fd.get() != fd2->get());
+}
+
+TEST_CASE("fd: dup2", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ REQUIRE(!fd_is_open(666));
+
+ auto fd = nihil::fd(file);
+ auto fd2 = dup(fd, 666);
+
+ REQUIRE(fd);
+ REQUIRE(fd2);
+ REQUIRE(fd2->get() == 666);
+}
+
+TEST_CASE("fd: flags", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+
+ {
+ auto const ret = replaceflags(fd, 0);
+ REQUIRE(ret);
+ REQUIRE(getflags(fd) == 0);
+ }
+
+ {
+ auto const ret = setflags(fd, O_NONBLOCK);
+ REQUIRE(ret == O_NONBLOCK);
+ REQUIRE(getflags(fd) == O_NONBLOCK);
+ }
+
+ {
+ auto const ret = setflags(fd, O_SYNC);
+ REQUIRE(ret == (O_NONBLOCK|O_SYNC));
+ REQUIRE(getflags(fd) == (O_NONBLOCK|O_SYNC));
+ }
+
+ {
+ auto const ret = clearflags(fd, O_NONBLOCK);
+ REQUIRE(ret == O_SYNC);
+ REQUIRE(getflags(fd) == O_SYNC);
+ }
+}
+
+TEST_CASE("fd: fdflags", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+
+ {
+ auto const ret = replacefdflags(fd, 0);
+ REQUIRE(ret);
+ REQUIRE(getfdflags(fd) == 0);
+ }
+
+ {
+ auto const ret = setfdflags(fd, FD_CLOEXEC);
+ REQUIRE(ret == FD_CLOEXEC);
+ REQUIRE(getfdflags(fd) == FD_CLOEXEC);
+ }
+
+ {
+ auto const ret = clearfdflags(fd, FD_CLOEXEC);
+ REQUIRE(ret == 0);
+ REQUIRE(getfdflags(fd) == 0);
+ }
+}
+
+TEST_CASE("fd: pipe, read, write", "[fd]") {
+ auto fds = nihil::pipe();
+ REQUIRE(fds);
+
+ auto [fd1, fd2] = std::move(*fds);
+
+ auto constexpr test_string = "test string"sv;
+
+ auto ret = write(fd1, test_string);
+ REQUIRE(ret);
+ REQUIRE(*ret == test_string.size());
+
+ auto readbuf = std::array<char, test_string.size() * 2>{};
+ ret = read(fd2, readbuf);
+ REQUIRE(ret);
+ REQUIRE(*ret == test_string.size());
+
+ auto read_string = std::string_view(std::span(readbuf).subspan(0, *ret));
+ REQUIRE(read_string == test_string);
+}
diff --git a/tests/generator.cc b/tests/generator.cc
new file mode 100644
index 0000000..8657756
--- /dev/null
+++ b/tests/generator.cc
@@ -0,0 +1,56 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <coroutine>
+#include <ranges>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("generator: basic", "[generator]")
+{
+ auto fn = [] () -> nihil::generator<int> {
+ co_yield 1;
+ co_yield 2;
+ co_yield 3;
+ };
+
+ auto values = std::vector<int>();
+ std::ranges::copy(fn(), std::back_inserter(values));
+
+ REQUIRE(values == std::vector{1, 2, 3});
+}
+
+TEST_CASE("generator: exceptions", "[generator]")
+{
+ auto fn = [] () -> nihil::generator<int> {
+ co_yield 1;
+ throw std::runtime_error("test");
+ };
+
+ auto range = fn();
+ auto it = std::ranges::begin(range);
+ REQUIRE(*it == 1);
+ REQUIRE_THROWS_AS(it++, std::runtime_error);
+}
+
+TEST_CASE("generator: elements_of", "[generator]")
+{
+ auto fn1 = [] -> nihil::generator<int> {
+ co_yield 1;
+ co_yield 2;
+ co_yield 3;
+ };
+
+ auto fn2 = [&fn1] -> nihil::generator<int> {
+ co_yield nihil::ranges::elements_of(fn1());
+ };
+
+ auto values = std::vector<int>();
+ std::ranges::copy(fn2(), std::back_inserter(values));
+
+ REQUIRE(values == std::vector{1, 2, 3});
+}
diff --git a/tests/generic_error.cc b/tests/generic_error.cc
new file mode 100644
index 0000000..b213af9
--- /dev/null
+++ b/tests/generic_error.cc
@@ -0,0 +1,17 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+using namespace std::literals;
+
+TEST_CASE("generic_error: basic", "[generic_error]") {
+ try {
+ throw nihil::generic_error("{} + {} = {}", 1, 2, 3);
+ } catch (nihil::generic_error const &exc) {
+ REQUIRE(exc.what() == "1 + 2 = 3"s);
+ }
+}
diff --git a/tests/getenv.cc b/tests/getenv.cc
new file mode 100644
index 0000000..adfa84f
--- /dev/null
+++ b/tests/getenv.cc
@@ -0,0 +1,48 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <ranges>
+#include <string>
+
+#include <unistd.h>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("getenv: existing value", "[getenv]")
+{
+ auto constexpr *name = "LFJAIL_TEST_VAR";
+ auto constexpr *value = "test is a test";
+
+ REQUIRE(::setenv(name, value, 1) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(s);
+ REQUIRE(*s == value);
+}
+
+TEST_CASE("getenv: non-existing value", "[getenv]")
+{
+ auto constexpr *name = "LFJAIL_TEST_VAR";
+
+ REQUIRE(::unsetenv(name) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(!s);
+ REQUIRE(s.error() == std::errc::no_such_file_or_directory);
+}
+
+// Force the call to getenv_r() to reallocate.
+TEST_CASE("getenv: long value")
+{
+ auto constexpr *name = "LFJAIL_TEST_VAR";
+ auto const value = std::string(4096, 'a');
+
+ REQUIRE(::setenv(name, value.c_str(), 1) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(s);
+ REQUIRE(*s == value);
+}
diff --git a/tests/guard.cc b/tests/guard.cc
new file mode 100644
index 0000000..f88aa9b
--- /dev/null
+++ b/tests/guard.cc
@@ -0,0 +1,20 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+using namespace std::literals;
+
+TEST_CASE("guard: basic", "[guard]") {
+ int n = 0;
+
+ {
+ auto guard = nihil::guard([&] { n = 1; });
+ REQUIRE(n == 0);
+ }
+
+ REQUIRE(n == 1);
+}
diff --git a/tests/tabulate.cc b/tests/tabulate.cc
new file mode 100644
index 0000000..84f8b33
--- /dev/null
+++ b/tests/tabulate.cc
@@ -0,0 +1,75 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <iterator>
+#include <string>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+using namespace std::literals;
+using namespace nihil;
+
+TEST_CASE("tabulate: basic", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "foo", "b"},
+ std::vector{"bar", "c", "baz"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {:2} {:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a foo b\n"
+"bar c baz\n");
+}
+
+TEST_CASE("tabulate: basic wide", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{L"a", L"foo", L"b"},
+ std::vector{L"bar", L"c", L"baz"},
+ };
+
+ auto result = std::wstring();
+ wtabulate(L"{:1} {:2} {:3}", input, std::back_inserter(result));
+
+ REQUIRE(result ==
+L"1 2 3\n"
+"a foo b\n"
+"bar c baz\n");
+}
+
+TEST_CASE("tabulate: jagged", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "foo", "b"},
+ std::vector{"bar", "baz"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {:2} {:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a foo b\n"
+"bar baz\n");
+}
+
+TEST_CASE("tabulate: align", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "longvalue", "s"},
+ std::vector{"a", "s", "longvalue"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {<:2} {>:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a longvalue s\n"
+"a s longvalue\n");
+}