aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/exess/test
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2021-02-25 10:27:59 -0500
committerDavid Robillard <d@drobilla.net>2021-03-08 23:23:05 -0500
commitc4821c8e6bf1f81c6ea31e11ebc0fc1666e9337b (patch)
treea62995534f5f606ac2f8bae22d525532b824cb5e /subprojects/exess/test
parent6bcd18ae60482790b645a345f718e7099250f261 (diff)
downloadserd-c4821c8e6bf1f81c6ea31e11ebc0fc1666e9337b.tar.gz
serd-c4821c8e6bf1f81c6ea31e11ebc0fc1666e9337b.tar.bz2
serd-c4821c8e6bf1f81c6ea31e11ebc0fc1666e9337b.zip
Add exess from git@gitlab.com:drobilla/exess.git 4638b1f
Diffstat (limited to 'subprojects/exess/test')
-rw-r--r--subprojects/exess/test/.clang-tidy11
-rw-r--r--subprojects/exess/test/float_test_data.h130
-rw-r--r--subprojects/exess/test/int_test_data.h42
-rw-r--r--subprojects/exess/test/meson.build76
-rw-r--r--subprojects/exess/test/num_test_utils.h85
-rw-r--r--subprojects/exess/test/test_base64.c196
-rw-r--r--subprojects/exess/test/test_bigint.c841
-rw-r--r--subprojects/exess/test/test_boolean.c114
-rw-r--r--subprojects/exess/test/test_byte.c98
-rw-r--r--subprojects/exess/test/test_canonical.c412
-rw-r--r--subprojects/exess/test/test_coerce.c519
-rw-r--r--subprojects/exess/test/test_datatype.c81
-rw-r--r--subprojects/exess/test/test_date.c257
-rw-r--r--subprojects/exess/test/test_datetime.c459
-rw-r--r--subprojects/exess/test/test_decimal.c260
-rw-r--r--subprojects/exess/test/test_double.c246
-rw-r--r--subprojects/exess/test/test_duration.c310
-rw-r--r--subprojects/exess/test/test_float.c240
-rw-r--r--subprojects/exess/test/test_hex.c180
-rw-r--r--subprojects/exess/test/test_int.c129
-rw-r--r--subprojects/exess/test/test_int_math.c97
-rw-r--r--subprojects/exess/test/test_long.c144
-rw-r--r--subprojects/exess/test/test_short.c100
-rw-r--r--subprojects/exess/test/test_strerror.c35
-rw-r--r--subprojects/exess/test/test_time.c222
-rw-r--r--subprojects/exess/test/test_timezone.c178
-rw-r--r--subprojects/exess/test/test_ubyte.c100
-rw-r--r--subprojects/exess/test/test_uint.c128
-rw-r--r--subprojects/exess/test/test_ulong.c126
-rw-r--r--subprojects/exess/test/test_ushort.c100
-rw-r--r--subprojects/exess/test/test_variant.c301
-rw-r--r--subprojects/exess/test/time_test_utils.h52
32 files changed, 6269 insertions, 0 deletions
diff --git a/subprojects/exess/test/.clang-tidy b/subprojects/exess/test/.clang-tidy
new file mode 100644
index 00000000..98645dc0
--- /dev/null
+++ b/subprojects/exess/test/.clang-tidy
@@ -0,0 +1,11 @@
+Checks: >
+ *,
+ -*-magic-numbers,
+ -*-uppercase-literal-suffix,
+ -clang-analyzer-nullability.NullableDereferenced,
+ -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
+ -llvm-header-guard,
+ -llvmlibc-*,
+WarningsAsErrors: '*'
+HeaderFilterRegex: '.*'
+FormatStyle: file
diff --git a/subprojects/exess/test/float_test_data.h b/subprojects/exess/test/float_test_data.h
new file mode 100644
index 00000000..eaea65c4
--- /dev/null
+++ b/subprojects/exess/test/float_test_data.h
@@ -0,0 +1,130 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "attributes.h"
+#include "ieee_float.h"
+#include "warnings.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdint.h>
+#include <string.h>
+
+/// Return the float with representation `rep`
+static inline float
+float_from_rep(const uint32_t rep)
+{
+ float f = 0.0f;
+ memcpy(&f, &rep, sizeof(f));
+ return f;
+}
+
+/// Return the double with representation `rep`
+static inline double
+double_from_rep(const uint64_t rep)
+{
+ double d = 0.0;
+ memcpy(&d, &rep, sizeof(d));
+ return d;
+}
+
+/// Return the distance between two doubles in ULPs
+static EXESS_I_PURE_FUNC uint64_t
+double_ulp_distance(const double a, const double b)
+{
+ assert(a >= 0.0);
+ assert(b >= 0.0);
+
+ const uint64_t ia = double_to_rep(a);
+ const uint64_t ib = double_to_rep(b);
+ if (ia == ib) {
+ return 0;
+ }
+
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ if (isnan(a) || isnan(b) || isinf(a) || isinf(b)) {
+ return UINT64_MAX;
+ }
+ EXESS_RESTORE_WARNINGS
+
+ return ia > ib ? ia - ib : ib - ia;
+}
+
+/// Return the distance between two floats in ULPs
+static EXESS_I_PURE_FUNC uint32_t
+float_ulp_distance(const float a, const float b)
+{
+ assert(a >= 0.0f);
+ assert(b >= 0.0f);
+
+ const uint32_t ia = float_to_rep(a);
+ const uint32_t ib = float_to_rep(b);
+ if (ia == ib) {
+ return 0;
+ }
+
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ if (isnan(a) || isnan(b) || isinf(a) || isinf(b)) {
+ return UINT32_MAX;
+ }
+ EXESS_RESTORE_WARNINGS
+
+ return ia > ib ? ia - ib : ib - ia;
+}
+
+static inline bool
+float_matches(const float a, const float b)
+{
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ const bool a_is_nan = isnan(a);
+ const bool a_is_negative = signbit(a);
+ const bool b_is_nan = isnan(b);
+ const bool b_is_negative = signbit(b);
+ EXESS_RESTORE_WARNINGS
+
+ if (a_is_nan && b_is_nan) {
+ return true;
+ }
+
+ if (a_is_nan || b_is_nan || a_is_negative != b_is_negative) {
+ return false;
+ }
+
+ return a_is_negative ? float_ulp_distance(-a, -b) == 0
+ : float_ulp_distance(a, b) == 0;
+}
+
+static inline bool
+double_matches(const double a, const double b)
+{
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ const bool a_is_nan = isnan(a);
+ const bool a_is_negative = signbit(a);
+ const bool b_is_nan = isnan(b);
+ const bool b_is_negative = signbit(b);
+ EXESS_RESTORE_WARNINGS
+
+ if (a_is_nan && b_is_nan) {
+ return true;
+ }
+
+ if (a_is_nan || b_is_nan || a_is_negative != b_is_negative) {
+ return false;
+ }
+
+ return a_is_negative ? double_ulp_distance(-a, -b) == 0
+ : double_ulp_distance(a, b) == 0;
+}
diff --git a/subprojects/exess/test/int_test_data.h b/subprojects/exess/test/int_test_data.h
new file mode 100644
index 00000000..978f863b
--- /dev/null
+++ b/subprojects/exess/test/int_test_data.h
@@ -0,0 +1,42 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef INT_TEST_DATA_H
+#define INT_TEST_DATA_H
+
+#include <stdint.h>
+
+/// Linear Congruential Generator for making random 32-bit integers
+static inline uint32_t
+lcg32(const uint32_t i)
+{
+ static const uint32_t a = 134775813u;
+ static const uint32_t c = 1u;
+
+ return (a * i) + c;
+}
+
+/// Linear Congruential Generator for making random 64-bit integers
+static inline uint64_t
+lcg64(const uint64_t i)
+{
+ static const uint64_t a = 6364136223846793005ull;
+ static const uint64_t c = 1ull;
+
+ return (a * i) + c;
+}
+
+#endif // INT_TEST_DATA_H
diff --git a/subprojects/exess/test/meson.build b/subprojects/exess/test/meson.build
new file mode 100644
index 00000000..972793f0
--- /dev/null
+++ b/subprojects/exess/test/meson.build
@@ -0,0 +1,76 @@
+autoship = find_program('autoship', required: false)
+
+test_args = []
+
+if get_option('strict')
+ if cc.get_id() == 'clang'
+ test_args = [
+ '-Wno-float-equal',
+ ]
+ elif cc.get_id() == 'gcc'
+ test_args = [
+ '-Wno-float-equal',
+ '-Wno-suggest-attribute=pure',
+ ]
+ elif cc.get_id() == 'msvc'
+ test_args = [
+ '/wd4996', # POSIX name is deprecated
+ ]
+ endif
+endif
+
+private_tests = [
+ 'bigint',
+ 'int_math',
+]
+
+foreach unit : private_tests
+ test(unit,
+ executable('test_@0@'.format(unit),
+ 'test_@0@.c'.format(unit),
+ c_args: exess_c_args + prog_args + test_args,
+ dependencies: exess_static_dep,
+ include_directories: include_directories('../src')),
+ suite: 'private')
+endforeach
+
+public_tests = [
+ 'base64',
+ 'boolean',
+ 'byte',
+ 'canonical',
+ 'coerce',
+ 'datatype',
+ 'date',
+ 'datetime',
+ 'decimal',
+ 'double',
+ 'duration',
+ 'float',
+ 'hex',
+ 'int',
+ 'long',
+ 'short',
+ 'strerror',
+ 'time',
+ 'timezone',
+ 'ubyte',
+ 'uint',
+ 'ulong',
+ 'ushort',
+ 'variant',
+]
+
+foreach unit : public_tests
+ test(unit,
+ executable('test_@0@'.format(unit),
+ 'test_@0@.c'.format(unit),
+ c_args: exess_c_args + prog_args + test_args,
+ dependencies: [m_dep, exess_dep],
+ include_directories: include_directories('../src')),
+ suite: 'public')
+endforeach
+
+if autoship.found()
+ test('autoship', autoship, args: ['test', exess_src_root], suite: 'data')
+endif
diff --git a/subprojects/exess/test/num_test_utils.h b/subprojects/exess/test/num_test_utils.h
new file mode 100644
index 00000000..ca29a479
--- /dev/null
+++ b/subprojects/exess/test/num_test_utils.h
@@ -0,0 +1,85 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+
+#ifdef _WIN32
+# include <process.h>
+#else
+# include <unistd.h>
+#endif
+
+typedef struct {
+ size_t n_tests;
+ uint32_t seed;
+ bool exhaustive;
+ bool error;
+} ExessNumTestOptions;
+
+static bool
+print_num_test_usage(const char* const name)
+{
+ fprintf(stderr, "Usage: %s [OPTION]...\n", name);
+ fprintf(stderr, " -n NUM_TESTS Number of random tests to run.\n");
+ fprintf(stderr, " -s SEED Use random seed.\n");
+ fprintf(stderr, " -x Exhaustively test numbers.\n");
+ return true;
+}
+
+static ExessNumTestOptions
+parse_num_test_options(const int argc, char* const* const argv)
+{
+ ExessNumTestOptions opts = {
+ 16384u, (uint32_t)time(NULL) + (uint32_t)getpid(), false, false};
+
+ int a = 1;
+ for (; a < argc && argv[a][0] == '-'; ++a) {
+ if (argv[a][1] == 'x') {
+ opts.exhaustive = true;
+ } else if (argv[a][1] == 's') {
+ if (++a == argc) {
+ opts.error = print_num_test_usage(argv[0]);
+ break;
+ }
+
+ opts.seed = (uint32_t)strtol(argv[a], NULL, 10);
+ } else if (argv[a][1] == 'n') {
+ if (++a == argc) {
+ opts.error = print_num_test_usage(argv[0]);
+ break;
+ }
+
+ opts.n_tests = (uint32_t)strtol(argv[a], NULL, 10);
+ } else {
+ opts.error = print_num_test_usage(argv[0]);
+ break;
+ }
+ }
+
+ return opts;
+}
+
+static void
+print_num_test_progress(const uint64_t i, const uint64_t n_tests)
+{
+ if (i % (n_tests / 20) == 1) {
+ fprintf(stderr, "%f%%\n", (double)i / (double)n_tests * 100.0);
+ }
+}
diff --git a/subprojects/exess/test/test_base64.c b/subprojects/exess/test/test_base64.c
new file mode 100644
index 00000000..760f44fe
--- /dev/null
+++ b/subprojects/exess/test/test_base64.c
@@ -0,0 +1,196 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_value_length,
+ const char* const expected_value,
+ const size_t expected_value_size,
+ const size_t expected_count)
+{
+ char buf[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
+ ExessBlob blob = {9, buf};
+
+ ExessResult r = exess_read_base64(&blob, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(r.status || blob.size == expected_value_size);
+ if (expected_value_length > 0) {
+ assert(!strncmp(buf, expected_value, expected_value_length));
+ assert(blob.size <= exess_base64_decoded_size(strlen(string)));
+ }
+}
+
+static void
+test_rfc4648_cases(void)
+{
+ char buf[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ assert(!exess_write_base64(6, "foobar", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm9vYmFy"));
+
+ assert(!exess_write_base64(5, "fooba", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm9vYmE="));
+
+ assert(!exess_write_base64(4, "foob", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm9vYg=="));
+
+ assert(!exess_write_base64(3, "foo", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm9v"));
+
+ assert(!exess_write_base64(2, "fo", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm8="));
+
+ assert(!exess_write_base64(1, "f", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zg=="));
+}
+
+static void
+test_whitespace(void)
+{
+ check_read("Zm9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 8);
+ check_read(" Zm9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Z\fm9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Zm\n9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Zm9\rvYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Zm9v\tYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Zm9vY\vmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read(" \f\n\r\t\vZm9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 14);
+ check_read("Zm9vYmFy \f\n\r\t\v", EXESS_SUCCESS, 6, "foobar", 6, 14);
+}
+
+static void
+test_syntax_errors(void)
+{
+ check_read("Z", EXESS_EXPECTED_BASE64, 0, NULL, 0, 1);
+ check_read("ZZ", EXESS_EXPECTED_BASE64, 0, NULL, 0, 2);
+ check_read("ZZZ", EXESS_EXPECTED_BASE64, 0, NULL, 0, 3);
+
+ check_read("=ZZZ", EXESS_BAD_VALUE, 0, NULL, 0, 4);
+ check_read("Z=ZZ", EXESS_BAD_VALUE, 0, NULL, 0, 4);
+ check_read("ZZ=Z", EXESS_BAD_VALUE, 0, NULL, 0, 4);
+
+ check_read("!m9vYmFy", EXESS_EXPECTED_BASE64, 0, NULL, 0, 0);
+ check_read("Z!9vYmFy", EXESS_EXPECTED_BASE64, 0, NULL, 0, 1);
+ check_read("Zm!vYmFy", EXESS_EXPECTED_BASE64, 0, NULL, 0, 2);
+ check_read("Zm9!YmFy", EXESS_EXPECTED_BASE64, 0, NULL, 0, 3);
+ check_read("Zm9v!mFy", EXESS_EXPECTED_BASE64, 0, NULL, 3, 4);
+ check_read("Zm9vY!Fy", EXESS_EXPECTED_BASE64, 0, NULL, 3, 5);
+ check_read("Zm9vYm!y", EXESS_EXPECTED_BASE64, 0, NULL, 3, 6);
+ check_read("Zm9vYmF!", EXESS_EXPECTED_BASE64, 0, NULL, 3, 7);
+}
+
+static void
+test_read_overflow(void)
+{
+ char buf[3] = {0, 0, 0};
+ ExessBlob blob0 = {0, buf};
+ ExessBlob blob1 = {1, buf};
+ ExessBlob blob2 = {2, buf};
+ ExessBlob blob3 = {3, buf};
+
+ ExessResult r = exess_read_base64(&blob0, "Zm9v");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 4);
+ assert(blob0.size == 0);
+
+ r = exess_read_base64(&blob1, "Zm9v");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 4);
+ assert(!buf[0]);
+
+ r = exess_read_base64(&blob2, "Zm9v");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 4);
+ assert(!buf[0]);
+
+ r = exess_read_base64(&blob3, "Zm9v");
+ assert(r.status == EXESS_SUCCESS);
+ assert(r.count == 4);
+ assert(blob3.size == 3);
+ assert(!strncmp(buf, "foo", 3));
+}
+
+static void
+test_write_overflow(void)
+{
+ char buf[5] = {1, 2, 3, 4, 5};
+
+ assert(exess_write_base64(3, "foo", 0, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 1, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 2, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 3, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 4, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 5, buf).status == EXESS_SUCCESS);
+}
+
+static void
+test_round_trip(void)
+{
+ for (size_t size = 1; size < 256; ++size) {
+ // Allocate and generate data
+ uint8_t* const data = (uint8_t*)malloc(size);
+ for (size_t i = 0; i < size; ++i) {
+ data[i] = (uint8_t)((size + i) % 256);
+ }
+
+ // Allocate buffer for encoding with minimum required size
+ const size_t str_len = exess_write_base64(size, data, 0, NULL).count;
+ char* const str = (char*)malloc(str_len + 1);
+
+ // Encode data to string buffer
+ assert(!exess_write_base64(size, data, str_len + 1, str).status);
+ assert(strlen(str) == str_len);
+ assert(str_len % 4 == 0);
+
+ // Allocate buffer for decoded data with the same size as the input
+ uint8_t* const decoded = (uint8_t*)malloc(size);
+ ExessBlob decoded_blob = {size, decoded};
+
+ // Decode and check that data matches the original input
+ assert(!exess_read_base64(&decoded_blob, str).status);
+ assert(decoded_blob.size == size);
+ assert(!memcmp(decoded, data, size));
+
+ free(decoded);
+ free(str);
+ free(data);
+ }
+}
+
+int
+main(void)
+{
+ test_rfc4648_cases();
+ test_whitespace();
+ test_syntax_errors();
+ test_read_overflow();
+ test_write_overflow();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_bigint.c b/subprojects/exess/test/test_bigint.c
new file mode 100644
index 00000000..3a74bb92
--- /dev/null
+++ b/subprojects/exess/test/test_bigint.c
@@ -0,0 +1,841 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "bigint.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+/* Some test data borrowed from http://github.com/google/double-conversion
+ which uses a completely different bigint representation (so if these agree,
+ everything is probably fine). Others cases are either made up to hit the
+ edges of the implementation, or interesting cases collected from testing the
+ decimal implementation. Almost everything here uses the hex representation
+ so it is easy to dump into Python as a sanity check. */
+
+static ExessBigint
+bigint_from_hex(const char* str)
+{
+ ExessBigint num;
+ exess_bigint_set_hex_string(&num, str);
+ return num;
+}
+
+static bool
+check_hex_equals(const char* const str, const ExessBigint* const num)
+{
+ const ExessBigint expected = bigint_from_hex(str);
+
+ return exess_bigint_compare(&expected, num) == 0;
+}
+
+#define CHECK_HEXEQ(str, num) assert(check_hex_equals(str, num))
+
+#define CHECK_SET(setter, value, expected) \
+ do { \
+ ExessBigint num; \
+ exess_bigint_set_##setter(&num, value); \
+ CHECK_HEXEQ(expected, &num); \
+ } while (0)
+
+static void
+test_set(void)
+{
+ CHECK_SET(u32, 0, "0");
+ CHECK_SET(u32, 0xA, "A");
+ CHECK_SET(u32, 0x20, "20");
+ CHECK_SET(u32, 0x12345678, "12345678");
+
+ CHECK_SET(u64, 0, "0");
+ CHECK_SET(u64, 0xA, "A");
+ CHECK_SET(u64, 0x20, "20");
+ CHECK_SET(u64, 0x12345678, "12345678");
+ CHECK_SET(u64, 0xFFFFFFFFFFFFFFFFull, "FFFFFFFFFFFFFFFF");
+ CHECK_SET(u64, 0x123456789ABCDEF0ull, "123456789ABCDEF0");
+ CHECK_SET(u64, 0x123456789ABCDEF0ull, "123456789ABCDEF0");
+
+ CHECK_SET(decimal_string, "0", "0");
+ CHECK_SET(decimal_string, "1", "1");
+ CHECK_SET(decimal_string, "01234567890", "499602D2");
+ CHECK_SET(decimal_string, "12345.67890", "499602D2");
+ CHECK_SET(decimal_string, "12345.67890EOF", "499602D2");
+ CHECK_SET(decimal_string, "012345678901", "2DFDC1C35");
+ CHECK_SET(decimal_string, "12345.678901", "2DFDC1C35");
+ CHECK_SET(decimal_string,
+ "340282366920938463463374607431768211456",
+ "100000000000000000000000000000000");
+
+ CHECK_SET(hex_string, "0", "0");
+ CHECK_SET(hex_string, "123456789ABCDEF0", "123456789ABCDEF0");
+
+ const ExessBigint orig = bigint_from_hex("123456789ABCDEF01");
+ ExessBigint copy;
+ exess_bigint_set(&copy, &orig);
+ CHECK_HEXEQ("123456789ABCDEF01", &copy);
+}
+
+static void
+check_left_shifted_bigit(const char* value,
+ const unsigned amount,
+ const unsigned index,
+ const Bigit expected)
+{
+ const ExessBigint num = bigint_from_hex(value);
+ const Bigit actual = exess_bigint_left_shifted_bigit(&num, amount, index);
+
+ assert(expected == actual);
+}
+
+static void
+test_left_shifted_bigit(void)
+{
+ check_left_shifted_bigit("0", 100, 1, 0x0);
+ check_left_shifted_bigit("1", 0, 0, 0x1);
+ check_left_shifted_bigit("1", 1, 0, 0x2);
+ check_left_shifted_bigit("1", 4, 0, 0x10);
+ check_left_shifted_bigit("1", 32, 0, 0x0);
+ check_left_shifted_bigit("1", 32, 1, 0x1);
+ check_left_shifted_bigit("1", 64, 0, 0x0);
+ check_left_shifted_bigit("1", 64, 1, 0x0);
+ check_left_shifted_bigit("1", 64, 2, 0x1);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 0, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 1, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 2, 0x89ABCDEF);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 3, 0x1234567);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 4, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 0, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 1, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 2, 0x13579BDE);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 3, 0x2468ACF);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 4, 0x0);
+}
+
+static void
+check_shift_left(const char* value, const unsigned amount, const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_shift_left(&num, amount);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_shift_left(void)
+{
+ check_shift_left("0", 100, "0");
+ check_shift_left("1", 1, "2");
+ check_shift_left("1", 4, "10");
+ check_shift_left("1", 32, "100000000");
+ check_shift_left("1", 64, "10000000000000000");
+ check_shift_left("123456789ABCDEF", 0, "123456789ABCDEF");
+ check_shift_left("123456789ABCDEF", 64, "123456789ABCDEF0000000000000000");
+ check_shift_left("123456789ABCDEF", 65, "2468ACF13579BDE0000000000000000");
+ check_shift_left("16B8B5E06EDC79", 23, "B5C5AF0376E3C800000");
+}
+
+static void
+check_add_u32(const char* value, const uint32_t rhs, const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_add_u32(&num, rhs);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_add_u32(void)
+{
+ check_add_u32("0", 1, "1");
+ check_add_u32("1", 1, "2");
+ check_add_u32("FFFFFFF", 1, "10000000");
+ check_add_u32("FFFFFFFFFFFFFF", 1, "100000000000000");
+
+ check_add_u32("10000000000000000000000000000000000080000000",
+ 0x80000000,
+ "10000000000000000000000000000000000100000000");
+
+ check_add_u32("10000000000000000000000000000000000000000000",
+ 0x1,
+ "10000000000000000000000000000000000000000001");
+}
+
+static void
+check_add(const char* lhs_hex, const char* rhs_hex, const char* expected)
+{
+ ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+
+ exess_bigint_add(&lhs, &rhs);
+ CHECK_HEXEQ(expected, &lhs);
+}
+
+static void
+test_add(void)
+{
+ check_add("1", "0", "1");
+ check_add("1", "1", "2");
+ check_add("FFFFFFF", "1", "10000000");
+ check_add("FFFFFFFFFFFFFF", "1", "100000000000000");
+ check_add("1", "1000000000000", "1000000000001");
+ check_add("FFFFFFF", "1000000000000", "100000FFFFFFF");
+
+ check_add("10000000000000000000000000000000000000000000",
+ "1",
+ "10000000000000000000000000000000000000000001");
+
+ check_add("10000000000000000000000000000000000000000000",
+ "1000000000000",
+ "10000000000000000000000000000001000000000000");
+
+ check_add("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+ "1000000000000",
+ "1000000000000000000000000000000FFFFFFFFFFFF");
+
+ check_add("10000000000000000000000000",
+ "1000000000000",
+ "10000000000001000000000000");
+
+ check_add(
+ "1", "10000000000000000000000000000", "10000000000000000000000000001");
+
+ check_add("FFFFFFF",
+ "10000000000000000000000000000",
+ "1000000000000000000000FFFFFFF");
+
+ check_add("10000000000000000000000000000000000000000000",
+ "10000000000000000000000000000",
+ "10000000000000010000000000000000000000000000");
+
+ check_add("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+ "10000000000000000000000000000",
+ "100000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFF");
+
+ check_add("10000000000000000000000000",
+ "10000000000000000000000000000",
+ "10010000000000000000000000000");
+}
+
+static void
+check_subtract(const char* lhs_hex, const char* rhs_hex, const char* expected)
+{
+ ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+
+ exess_bigint_subtract(&lhs, &rhs);
+ CHECK_HEXEQ(expected, &lhs);
+}
+
+static void
+test_subtract(void)
+{
+ check_subtract("1", "0", "1");
+ check_subtract("2", "0", "2");
+ check_subtract("10000000", "1", "FFFFFFF");
+ check_subtract("1FFFFFFFF00000000", "FFFFFFFF", "1FFFFFFFE00000001");
+ check_subtract("100000000000000", "1", "FFFFFFFFFFFFFF");
+ check_subtract("1000000000001", "1000000000000", "1");
+ check_subtract("100000FFFFFFF", "1000000000000", "FFFFFFF");
+
+ check_subtract(
+ "11F2678326EA00000000", "0878678326EAC9000000", "979FFFFFFFF37000000");
+
+ check_subtract("10000000000000000000000000000000000000000001",
+ "00000000000000000000000000000000000000000001",
+ "10000000000000000000000000000000000000000000");
+
+ check_subtract("10000000000000000000000000000001000000000000",
+ "00000000000000000000000000000001000000000000",
+ "10000000000000000000000000000000000000000000");
+
+ check_subtract("1000000000000000000000000000000FFFFFFFFFFFF",
+ "0000000000000000000000000000001000000000000",
+ " FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
+
+ check_subtract("10000000000000000000000000",
+ "00000000000001000000000000",
+ " FFFFFFFFFFFFF000000000000");
+
+ check_subtract("10000000000000000000000000",
+ "1000000000000000000000000",
+ "F000000000000000000000000");
+
+ check_subtract("FFFFFFF000000000000000",
+ "0000000000000800000000",
+ "FFFFFFEFFFFFF800000000");
+
+ check_subtract("10000000000000000000000000000000000000000000",
+ "00000000000000000000000000000000000800000000",
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000");
+
+ check_subtract("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+ "000000000000000000000000000000000800000000",
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFF");
+}
+
+static void
+check_subtract_left_shifted(const char* lhs_hex,
+ const char* rhs_hex,
+ const unsigned amount,
+ const char* expected)
+{
+ ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+
+ exess_bigint_subtract_left_shifted(&lhs, &rhs, amount);
+ CHECK_HEXEQ(expected, &lhs);
+}
+
+static void
+test_subtract_left_shifted(void)
+{
+ check_subtract_left_shifted("1", "0", 1, "1");
+ check_subtract_left_shifted("10000000", "1", 1, "FFFFFFE");
+ check_subtract_left_shifted("100000000", "40000000", 2, "0");
+ check_subtract_left_shifted("1000000000000000", "400000000000000", 2, "0");
+ check_subtract_left_shifted("1000000000000000", "800000000000000", 1, "0");
+ check_subtract_left_shifted("1000000000000000", "F", 16, "FFFFFFFFFF10000");
+ check_subtract_left_shifted("1000000000000000", "F", 24, "FFFFFFFF1000000");
+ check_subtract_left_shifted("100000000000000", "1", 0, "FFFFFFFFFFFFFF");
+ check_subtract_left_shifted("100000000000000", "1", 56, "0");
+
+ check_subtract_left_shifted(
+ "11F2678326EA00000000", "43C33C1937564800000", 1, "979FFFFFFFF37000000");
+}
+
+static void
+check_multiply_u32(const char* value, const uint32_t rhs, const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_multiply_u32(&num, rhs);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_multiply_u32(void)
+{
+ check_multiply_u32("0", 0x25, "0");
+ check_multiply_u32("123456789ABCDEF", 0, "0");
+ check_multiply_u32("2", 0x5, "A");
+ check_multiply_u32("10000000", 0x9, "90000000");
+ check_multiply_u32("100000000000000", 0xFFFF, "FFFF00000000000000");
+ check_multiply_u32("100000000000000", 0xFFFFFFFF, "FFFFFFFF00000000000000");
+ check_multiply_u32("1234567ABCD", 0xFFF, "12333335552433");
+ check_multiply_u32("1234567ABCD", 0xFFFFFFF, "12345679998A985433");
+ check_multiply_u32("FFFFFFFFFFFFFFFF", 0x2, "1FFFFFFFFFFFFFFFE");
+ check_multiply_u32("FFFFFFFFFFFFFFFF", 0x4, "3FFFFFFFFFFFFFFFC");
+ check_multiply_u32("FFFFFFFFFFFFFFFF", 0xF, "EFFFFFFFFFFFFFFF1");
+ check_multiply_u32("FFFFFFFFFFFFFFFF", 0xFFFFFF, "FFFFFEFFFFFFFFFF000001");
+ check_multiply_u32("377654D193A171", 10000000, "210EDD6D4CDD2580EE80");
+ check_multiply_u32("2E3F36D108373C00000", 10, "1CE78242A52285800000");
+
+ check_multiply_u32(
+ "10000000000000000000000000", 0x00000002, "20000000000000000000000000");
+
+ check_multiply_u32(
+ "10000000000000000000000000", 0x0000000F, "F0000000000000000000000000");
+
+ check_multiply_u32("FFFF0000000000000000000000000",
+ 0xFFFF,
+ "FFFE00010000000000000000000000000");
+
+ check_multiply_u32("FFFF0000000000000000000000000",
+ 0xFFFFFFFF,
+ "FFFEFFFF00010000000000000000000000000");
+
+ check_multiply_u32("FFFF0000000000000000000000000",
+ 0xFFFFFFFF,
+ "FFFEFFFF00010000000000000000000000000");
+}
+
+static void
+check_multiply_u64(const char* value, const uint64_t rhs, const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_multiply_u64(&num, rhs);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_multiply_u64(void)
+{
+ check_multiply_u64("0", 0x25, "0");
+ check_multiply_u64("123456789ABCDEF", 0, "0");
+ check_multiply_u64("123456789ABCDEF", 1, "123456789ABCDEF");
+ check_multiply_u64("2", 0x5, "A");
+ check_multiply_u64("10000000", 0x9, "90000000");
+ check_multiply_u64("100000000000000", 0xFFFF, "FFFF00000000000000");
+ check_multiply_u64("1234567ABCD", 0xFFF, "12333335552433");
+ check_multiply_u64("1234567ABCD", 0xFFFFFFFFFFull, "1234567ABCBDCBA985433");
+ check_multiply_u64("FFFFFFFFFFFFFFFF", 0x2, "1FFFFFFFFFFFFFFFE");
+ check_multiply_u64("FFFFFFFFFFFFFFFF", 0x4, "3FFFFFFFFFFFFFFFC");
+ check_multiply_u64("FFFFFFFFFFFFFFFF", 0xF, "EFFFFFFFFFFFFFFF1");
+
+ check_multiply_u64(
+ "100000000000000", 0xFFFFFFFFFFFFFFFFull, "FFFFFFFFFFFFFFFF00000000000000");
+
+ check_multiply_u64("FFFFFFFFFFFFFFFF",
+ 0xFFFFFFFFFFFFFFFFull,
+ "FFFFFFFFFFFFFFFE0000000000000001");
+
+ check_multiply_u64(
+ "10000000000000000000000000", 0x00000002, "20000000000000000000000000");
+
+ check_multiply_u64(
+ "10000000000000000000000000", 0x0000000F, "F0000000000000000000000000");
+
+ check_multiply_u64("FFFF0000000000000000000000000",
+ 0xFFFF,
+ "FFFE00010000000000000000000000000");
+
+ check_multiply_u64("FFFF0000000000000000000000000",
+ 0xFFFFFFFF,
+ "FFFEFFFF00010000000000000000000000000");
+
+ check_multiply_u64("FFFF0000000000000000000000000",
+ 0xFFFFFFFFFFFFFFFFull,
+ "FFFEFFFFFFFFFFFF00010000000000000000000000000");
+
+ check_multiply_u64(
+ "377654D193A171", 0x8AC7230489E80000ull, "1E10EE4B11D15A7F3DE7F3C7680000");
+}
+
+static void
+check_multiply_pow10(const char* value,
+ const unsigned exponent,
+ const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_multiply_pow10(&num, exponent);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_multiply_pow10(void)
+{
+ check_multiply_pow10("0", 10, "0");
+ check_multiply_pow10("1234", 0, "1234");
+ check_multiply_pow10("4D2", 1, "3034");
+ check_multiply_pow10("4D2", 2, "1E208");
+ check_multiply_pow10("4D2", 3, "12D450");
+ check_multiply_pow10("4D2", 4, "BC4B20");
+ check_multiply_pow10("4D2", 5, "75AEF40");
+ check_multiply_pow10("4D2", 6, "498D5880");
+ check_multiply_pow10("4D2", 7, "2DF857500");
+ check_multiply_pow10("4D2", 8, "1CBB369200");
+ check_multiply_pow10("4D2", 9, "11F5021B400");
+ check_multiply_pow10("4D2", 10, "B3921510800");
+ check_multiply_pow10("4D2", 11, "703B4D2A5000");
+ check_multiply_pow10("4D2", 12, "4625103A72000");
+ check_multiply_pow10("4D2", 13, "2BD72A24874000");
+ check_multiply_pow10("4D2", 14, "1B667A56D488000");
+ check_multiply_pow10("4D2", 15, "11200C7644D50000");
+ check_multiply_pow10("4D2", 16, "AB407C9EB0520000");
+ check_multiply_pow10("4D2", 17, "6B084DE32E3340000");
+ check_multiply_pow10("4D2", 18, "42E530ADFCE0080000");
+ check_multiply_pow10("4D2", 19, "29CF3E6CBE0C0500000");
+ check_multiply_pow10("4D2", 20, "1A218703F6C783200000");
+ check_multiply_pow10("4D2", 21, "1054F4627A3CB1F400000");
+ check_multiply_pow10("4D2", 22, "A3518BD8C65EF38800000");
+ check_multiply_pow10("4D2", 23, "6612F7677BFB5835000000");
+ check_multiply_pow10("4D2", 24, "3FCBDAA0AD7D17212000000");
+ check_multiply_pow10("4D2", 25, "27DF68A46C6E2E74B4000000");
+ check_multiply_pow10("4D2", 26, "18EBA166C3C4DD08F08000000");
+ check_multiply_pow10("4D2", 27, "F9344E03A5B0A259650000000");
+ check_multiply_pow10("4D2", 28, "9BC0B0C2478E6577DF20000000");
+ check_multiply_pow10("4D2", 29, "61586E796CB8FF6AEB740000000");
+ check_multiply_pow10("4D2", 30, "3CD7450BE3F39FA2D32880000000");
+ check_multiply_pow10("4D2", 31, "26068B276E7843C5C3F9500000000");
+
+ check_multiply_pow10("4D2",
+ 50,
+ "149D1B4CFED03B23AB5F4E1196EF45C0"
+ "8000000000000");
+
+ check_multiply_pow10("4D2",
+ 100,
+ "5827249F27165024FBC47DFCA9359BF3"
+ "16332D1B91ACEECF471FBAB06D9B2000"
+ "0000000000000000000000");
+
+ check_multiply_pow10("4D2",
+ 305,
+ "AFBA390D657B0829339F5B98DC852A89"
+ "682758E01829EADFD016D1528D4D548B"
+ "80894B9ED9C2EC6A9CABB4881302A637"
+ "9FF3058908FEAC310C52FCA009799718"
+ "8260B0B2E2EC96E471B7892AD9B4F9F9"
+ "A448CBF150D2E87F3934000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000");
+
+ check_multiply_pow10("123456789ABCDEF0", 0, "123456789ABCDEF0");
+ check_multiply_pow10("123456789ABCDEF0",
+ 44,
+ "51A1AD66ACE4E5C79209330F58F52DE3"
+ "7CEFFF1F000000000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 88,
+ "16E0C6D18F4BFA7D0289B88382F56151"
+ "EB9DA5DB09D56C9BA5D8305619CEE057"
+ "4F00000000000000000000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 132,
+ "6696B1DA27BEA173B5EFCAABBB8492A9"
+ "2AE3D97F7EE3C7314FB7E2FF8AEFD329"
+ "F5F8202C22650BB79A7D9F3867F00000"
+ "00000000000000000000000000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 176,
+ "1CC05FF0499D8BC7D8EBE0C6DC2FDC09"
+ "E93765F3448235FB16AD09D98BBB3A0A"
+ "843372D33A318EE63DAE6998DA59EF34"
+ "B15C40A65B9B65ABF3CAF00000000000"
+ "00000000000000000000000000000000"
+ "00");
+ check_multiply_pow10("123456789ABCDEF0",
+ 220,
+ "80ED0FD9A6C0F56A495F466320D34E22"
+ "507FAA83F0519E7FF909FDDBDA184682"
+ "BB70D38D43284C828A3681540722E550"
+ "960567BAB1C25389C1BE7705228BE8CC"
+ "AF3EBD382829DF000000000000000000"
+ "00000000000000000000000000000000"
+ "000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 264,
+ "2421FD0F55C486D05211339D45EC2DC4"
+ "12AE7A64DDFE619DA81B73C069088D3E"
+ "83D7AA9F99B571815DE939A5275FB4A6"
+ "9D8930798C01FB96781B9D633BB59AD5"
+ "A7F322A7EC14154D1B8B5DF1718779A5"
+ "2291FE0F000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 308,
+ "A206620F35C83E9E780ECC07DCAF13BB"
+ "0A7EE2E213747914340BC172D783BA56"
+ "661E8DCFFD03C398BD66F5570F445AC6"
+ "737126283C64AE1A289B9D8BB4531033"
+ "8C3E34DE2D534187092ABA1F4706100E"
+ "ECF66D14059461A05A9BEBBCCBA0F693"
+ "F0000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000");
+}
+
+static void
+check_divmod(const char* lhs_hex,
+ const char* rhs_hex,
+ const uint32_t expected_divisor,
+ const char* expected_mod_hex)
+{
+ ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+ const uint32_t divisor = exess_bigint_divmod(&lhs, &rhs);
+
+ assert(divisor == expected_divisor);
+ CHECK_HEXEQ(expected_mod_hex, &lhs);
+}
+
+static void
+test_divmod(void)
+{
+ check_divmod("A", "2", 5, "0");
+ check_divmod("B", "2", 5, "1");
+ check_divmod("C", "2", 6, "0");
+ check_divmod("A", "1234567890", 0, "A");
+ check_divmod("FFFFFFFF", "3", 0x55555555, "0");
+ check_divmod("12345678", "3789012", 5, "D9861E");
+ check_divmod("70000001", "1FFFFFFF", 3, "10000004");
+ check_divmod("28000000", "12A05F20", 2, "2BF41C0");
+ check_divmod("FFFFFFFFF", "FFFFFFFF", 16, "F");
+ check_divmod("100000000000001", "FFFFFFF", 0x10000001, "2");
+ check_divmod("40000000000002", "2FAF0800000000", 1, "1050F800000002");
+ check_divmod("40000000000000", "40000000000000", 1, "0");
+
+ check_divmod("43DE72C3DF858FC278A361EEB5A000000",
+ "80000000000000000000000000000000",
+ 8,
+ "3DE72C3DF858FC278A361EEB5A000000");
+
+ check_divmod(
+ "B5C5AF0376E3C800000", "43C33C1937564800000", 2, "2E3F36D108373800000");
+
+ check_divmod("A0000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000",
+ "20000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000",
+ 5,
+ "0");
+
+ check_divmod("A0000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000001",
+ "20000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000",
+ 5,
+ "1");
+
+ check_divmod("B6080000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000FF"
+ "F",
+ "A0000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000",
+ 0x1234,
+ "FFF");
+
+ check_divmod("B6080000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "0",
+ "9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFF001",
+ 0x1234,
+ "1232DCC");
+}
+
+static void
+check_compare(const char* lhs_hex, const char* rhs_hex, const int expected_cmp)
+{
+ const ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+ const int cmp = exess_bigint_compare(&lhs, &rhs);
+
+ assert(cmp == expected_cmp);
+
+ if (cmp) {
+ const int rcmp = exess_bigint_compare(&rhs, &lhs);
+ assert(rcmp == -cmp);
+ }
+}
+
+static void
+test_compare(void)
+{
+ check_compare("1", "1", 0);
+ check_compare("0", "1", -1);
+ check_compare("F", "FF", -1);
+ check_compare("F", "FFFFFFFFF", -1);
+ check_compare("10000000000000000", "10000000000000000", 0);
+ check_compare("10000000000000000", "10000000000000001", -1);
+ check_compare("FFFFFFFFFFFFFFFFF", "100000000000000000", -1);
+ check_compare("10000000000000000", "10000000000000001", -1);
+ check_compare("1234567890ABCDEF12345", "1234567890ABCDEF12345", 0);
+ check_compare("1234567890ABCDEF12345", "1234567890ABCDEF12346", -1);
+
+ const char* const huge = "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0";
+
+ const char* const huger = "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF1";
+
+ check_compare(huge, huge, 0);
+ check_compare(huger, huger, 0);
+ check_compare(huge, huger, -1);
+}
+
+static void
+check_plus_compare(const char* l_hex,
+ const char* p_hex,
+ const char* c_hex,
+ const int expected_cmp)
+{
+ const ExessBigint l = bigint_from_hex(l_hex);
+ const ExessBigint p = bigint_from_hex(p_hex);
+ const ExessBigint c = bigint_from_hex(c_hex);
+ const int cmp = exess_bigint_plus_compare(&l, &p, &c);
+ const int rcmp = exess_bigint_plus_compare(&p, &l, &c);
+
+ assert(cmp == expected_cmp);
+ assert(rcmp == expected_cmp);
+}
+
+static void
+test_plus_compare(void)
+{
+ check_plus_compare("1", "0", "1", 0);
+ check_plus_compare("0", "0", "1", -1);
+ check_plus_compare("FFFFFFFFF", "F", "F", 1);
+ check_plus_compare("F", "F", "800000000", -1);
+ check_plus_compare("F", "F", "80000000000000000", -1);
+ check_plus_compare("800000000", "F", "80000000000000000", -1);
+ check_plus_compare("2D79883D20000", "2D79883D20000", "5AF3107A40000", 0);
+ check_plus_compare("20000000000000", "1", "20000000000000", +1);
+
+ check_plus_compare(
+ "0588A503282FE00000", "0588A503282FE00000", "0AD78EBC5AC6200000", +1);
+
+ check_plus_compare("2F06018572BEADD1280000000",
+ "0204FCE5E3E25026110000000",
+ "4000000000000000000000000",
+ -1);
+
+ check_plus_compare("1234567890ABCDEF12345",
+ "000000000000000000001",
+ "1234567890ABCDEF12345",
+ +1);
+
+ check_plus_compare("1234567890ABCDEF12344",
+ "000000000000000000001",
+ "1234567890ABCDEF12345",
+ 0);
+
+ check_plus_compare("123456789000000000000",
+ "0000000000ABCDEF12345",
+ "1234567890ABCDEF12345",
+ 0);
+
+ check_plus_compare("123456789000000000000",
+ "0000000000ABCDEF12344",
+ "1234567890ABCDEF12345",
+ -1);
+
+ check_plus_compare("123456789000000000000",
+ "0000000000ABCDEF12346",
+ "1234567890ABCDEF12345",
+ 1);
+
+ check_plus_compare("123456789100000000000",
+ "0000000000ABCDEF12345",
+ "1234567890ABCDEF12345",
+ 1);
+
+ check_plus_compare("123456788900000000000",
+ "0000000000ABCDEF12345",
+ "1234567890ABCDEF12345",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "0000000000ABCDEF1234500000000",
+ "1234567890ABCDEF1234500000000",
+ 0);
+
+ check_plus_compare("12345678900000000000000000000",
+ "0000000000ABCDEF1234400000000",
+ "1234567890ABCDEF1234500000000",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "0000000000ABCDEF1234600000000",
+ "1234567890ABCDEF1234500000000",
+ 1);
+
+ check_plus_compare("12345678910000000000000000000",
+ "0000000000ABCDEF1234500000000",
+ "1234567890ABCDEF1234500000000",
+ 1);
+ check_plus_compare("12345678890000000000000000000",
+ "0000000000ABCDEF1234500000000",
+ "1234567890ABCDEF1234500000000",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12345",
+ "123456789000000000ABCDEF12345",
+ 0);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12346",
+ "123456789000000000ABCDEF12345",
+ 1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12344",
+ "123456789000000000ABCDEF12345",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12345",
+ "12345678900000ABCDEF123450000",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12344",
+ "12345678900000ABCDEF123450000",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12345",
+ "12345678900000ABCDEF123450001",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "00000000000000ABCDEF123460000",
+ "12345678900000ABCDEF123450000",
+ 1);
+}
+
+static void
+check_pow10(const unsigned exponent, const char* expected)
+{
+ ExessBigint num;
+ exess_bigint_set_pow10(&num, exponent);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_set_pow10(void)
+{
+ check_pow10(0, "1");
+ check_pow10(1, "A");
+ check_pow10(2, "64");
+ check_pow10(5, "186A0");
+ check_pow10(8, "5F5E100");
+ check_pow10(16, "2386F26FC10000");
+ check_pow10(30, "C9F2C9CD04674EDEA40000000");
+ check_pow10(31, "7E37BE2022C0914B2680000000");
+}
+
+int
+main(void)
+{
+ test_set();
+ test_left_shifted_bigit();
+ test_shift_left();
+ test_add_u32();
+ test_add();
+ test_subtract();
+ test_subtract_left_shifted();
+ test_multiply_u32();
+ test_multiply_u64();
+ test_multiply_pow10();
+ test_divmod();
+ test_compare();
+ test_plus_compare();
+ test_set_pow10();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_boolean.c b/subprojects/exess/test/test_boolean.c
new file mode 100644
index 00000000..65eb9b24
--- /dev/null
+++ b/subprojects/exess/test/test_boolean.c
@@ -0,0 +1,114 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const bool expected_value,
+ const size_t expected_count)
+{
+ bool value = false;
+
+ const ExessResult r = exess_read_boolean(&value, string);
+ assert(value == expected_value);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+}
+
+static void
+test_read_boolean(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_BOOLEAN, false, 6);
+
+ // Canonical form
+ check_read("false", EXESS_SUCCESS, false, 5);
+ check_read("true", EXESS_SUCCESS, true, 4);
+
+ // Non-canonical form
+ check_read("0", EXESS_SUCCESS, false, 1);
+ check_read("1", EXESS_SUCCESS, true, 1);
+ check_read(" \f\n\r\t\vfalse ", EXESS_SUCCESS, false, 11);
+ check_read(" \f\n\r\t\vtrue ", EXESS_SUCCESS, true, 10);
+ check_read(" \f\n\r\t\v0 ", EXESS_SUCCESS, false, 7);
+ check_read(" \f\n\r\t\v1 ", EXESS_SUCCESS, true, 7);
+
+ // Trailing garbage
+ check_read("falsely", EXESS_EXPECTED_END, false, 5);
+ check_read("truely", EXESS_EXPECTED_END, true, 4);
+ check_read("0no", EXESS_EXPECTED_END, false, 1);
+ check_read("1yes", EXESS_EXPECTED_END, true, 1);
+
+ // Garbage
+ check_read("twue", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("fawse", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("tr", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("fa", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("yes", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("no", EXESS_EXPECTED_BOOLEAN, false, 0);
+}
+
+static void
+check_write(const bool value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_BOOLEAN_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_boolean(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_boolean(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_boolean(void)
+{
+ check_write(true, EXESS_SUCCESS, 5, "true");
+ check_write(false, EXESS_SUCCESS, 6, "false");
+
+ check_write(true, EXESS_NO_SPACE, 4, "");
+ check_write(false, EXESS_NO_SPACE, 5, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_boolean(false, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+int
+main(void)
+{
+ test_read_boolean();
+ test_write_boolean();
+ return 0;
+}
diff --git a/subprojects/exess/test/test_byte.c b/subprojects/exess/test/test_byte.c
new file mode 100644
index 00000000..a67fd779
--- /dev/null
+++ b/subprojects/exess/test/test_byte.c
@@ -0,0 +1,98 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int8_t expected_value,
+ const size_t expected_count)
+{
+ int8_t value = 0;
+ const ExessResult r = exess_read_byte(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_byte(void)
+{
+ // Limits
+ check_read("-128", EXESS_SUCCESS, INT8_MIN, EXESS_MAX_BYTE_LENGTH);
+ check_read("127", EXESS_SUCCESS, INT8_MAX, 3);
+
+ // Out of range
+ check_read("-129", EXESS_OUT_OF_RANGE, 0, 4);
+ check_read("128", EXESS_OUT_OF_RANGE, 0, 3);
+
+ // Garbage
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 1);
+}
+
+static void
+check_write(const int8_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_BYTE_LENGTH + 1] = {1, 2, 3, 4, 5};
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_byte(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(!strcmp(buf, expected_string));
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_byte(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_byte(void)
+{
+ check_write(INT8_MIN, EXESS_SUCCESS, 5, "-128");
+ check_write(INT8_MAX, EXESS_SUCCESS, 4, "127");
+}
+
+static void
+test_round_trip(void)
+{
+ int8_t value = 0;
+ char buf[EXESS_MAX_BYTE_LENGTH + 1] = {1, 2, 3, 4, 5};
+
+ for (int16_t i = INT8_MIN; i <= INT8_MAX; ++i) {
+ assert(!exess_write_byte((int8_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_byte(&value, buf).status);
+ assert(value == i);
+ }
+}
+
+int
+main(void)
+{
+ test_read_byte();
+ test_write_byte();
+ test_round_trip();
+ return 0;
+}
diff --git a/subprojects/exess/test/test_canonical.c b/subprojects/exess/test/test_canonical.c
new file mode 100644
index 00000000..77167fcf
--- /dev/null
+++ b/subprojects/exess/test/test_canonical.c
@@ -0,0 +1,412 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <string.h>
+
+static void
+check_write(const ExessDatatype datatype,
+ const char* const value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[328] = {42};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_canonical(value, datatype, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert((r.status && r.status != EXESS_EXPECTED_END) ||
+ exess_write_canonical(value, datatype, 0, NULL).count == r.count);
+}
+
+static void
+test_decimal(void)
+{
+ check_write(EXESS_DECIMAL, "", EXESS_EXPECTED_DIGIT, 1, "");
+ check_write(EXESS_DECIMAL, " \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 1, "");
+
+ check_write(EXESS_DECIMAL, " -001 ", EXESS_SUCCESS, 5, "-1.0");
+ check_write(EXESS_DECIMAL, " -000 ", EXESS_SUCCESS, 5, "-0.0");
+ check_write(EXESS_DECIMAL, " 000 ", EXESS_SUCCESS, 4, "0.0");
+ check_write(EXESS_DECIMAL, " 001 ", EXESS_SUCCESS, 4, "1.0");
+ check_write(EXESS_DECIMAL, " +001 ", EXESS_SUCCESS, 4, "1.0");
+
+ check_write(EXESS_DECIMAL, " -.123", EXESS_SUCCESS, 7, "-0.123");
+ check_write(EXESS_DECIMAL, " +.123", EXESS_SUCCESS, 6, "0.123");
+ check_write(EXESS_DECIMAL, " -0 ", EXESS_SUCCESS, 5, "-0.0");
+ check_write(EXESS_DECIMAL, " +0 ", EXESS_SUCCESS, 4, "0.0");
+
+ check_write(EXESS_DECIMAL, " +00.10 ", EXESS_SUCCESS, 4, "0.1");
+ check_write(EXESS_DECIMAL, " +01 ", EXESS_SUCCESS, 4, "1.0");
+
+ check_write(EXESS_DECIMAL,
+ " 36893488147419103232 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " 0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +36893488147419103232 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " -36893488147419103232 ",
+ EXESS_SUCCESS,
+ 24,
+ "-36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +0036893488147419103232. ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +0036893488147419103232.00 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +0036893488147419103232.12300 ",
+ EXESS_SUCCESS,
+ 25,
+ "36893488147419103232.123");
+
+ check_write(EXESS_DECIMAL,
+ " -0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 24,
+ "-36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " -0036893488147419103232. ",
+ EXESS_SUCCESS,
+ 24,
+ "-36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " -0036893488147419103232.00 ",
+ EXESS_SUCCESS,
+ 24,
+ "-36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " -0036893488147419103232.12300 ",
+ EXESS_SUCCESS,
+ 26,
+ "-36893488147419103232.123");
+
+ check_write(EXESS_DECIMAL, " -1234extra", EXESS_EXPECTED_END, 8, "-1234.0");
+ check_write(EXESS_DECIMAL, " 1234extra", EXESS_EXPECTED_END, 7, "1234.0");
+
+ check_write(EXESS_DECIMAL, " f", EXESS_EXPECTED_DIGIT, 2, "");
+ check_write(EXESS_DECIMAL, "", EXESS_EXPECTED_DIGIT, 1, "");
+}
+
+static void
+test_integer(void)
+{
+ check_write(EXESS_INTEGER, "", EXESS_EXPECTED_DIGIT, 1, "");
+ check_write(EXESS_INTEGER, " \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 1, "");
+
+ // Integer
+
+ check_write(EXESS_INTEGER, " -001 ", EXESS_SUCCESS, 3, "-1");
+ check_write(EXESS_INTEGER, " 000 ", EXESS_SUCCESS, 2, "0");
+ check_write(EXESS_INTEGER, " 001 ", EXESS_SUCCESS, 2, "1");
+ check_write(EXESS_INTEGER, " +001 ", EXESS_SUCCESS, 2, "1");
+
+ check_write(EXESS_INTEGER, " junk 987654321 ", EXESS_EXPECTED_DIGIT, 2, "");
+ check_write(
+ EXESS_INTEGER, " 987654321 junk ", EXESS_EXPECTED_END, 10, "987654321");
+
+ check_write(EXESS_INTEGER,
+ " 36893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " 0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " +36893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " +0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " -36893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " -0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ // NonPositiveInteger
+
+ check_write(EXESS_NON_POSITIVE_INTEGER, " -001 ", EXESS_SUCCESS, 3, "-1");
+ check_write(EXESS_NON_POSITIVE_INTEGER, " 000 ", EXESS_SUCCESS, 2, "0");
+ check_write(EXESS_NON_POSITIVE_INTEGER, " 001 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_NON_POSITIVE_INTEGER, " +001 ", EXESS_BAD_VALUE, 3, "");
+
+ check_write(EXESS_NON_POSITIVE_INTEGER,
+ " -36893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ check_write(EXESS_NON_POSITIVE_INTEGER,
+ " -0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ // NegativeInteger
+
+ check_write(EXESS_NEGATIVE_INTEGER, " -001 ", EXESS_SUCCESS, 3, "-1");
+ check_write(EXESS_NEGATIVE_INTEGER, " 000 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_NEGATIVE_INTEGER, " 001 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_NEGATIVE_INTEGER, " +001 ", EXESS_BAD_VALUE, 3, "");
+
+ check_write(EXESS_NEGATIVE_INTEGER,
+ " -36893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ check_write(EXESS_NEGATIVE_INTEGER,
+ " -0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ // NonNegativeInteger
+
+ check_write(EXESS_NON_NEGATIVE_INTEGER, " -001 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_NON_NEGATIVE_INTEGER, " 000 ", EXESS_SUCCESS, 2, "0");
+ check_write(EXESS_NON_NEGATIVE_INTEGER, " 001 ", EXESS_SUCCESS, 2, "1");
+ check_write(EXESS_NON_NEGATIVE_INTEGER, " +001 ", EXESS_SUCCESS, 2, "1");
+
+ check_write(EXESS_NON_NEGATIVE_INTEGER,
+ " 36893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_NON_NEGATIVE_INTEGER,
+ " 0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ // PositiveInteger
+
+ check_write(EXESS_POSITIVE_INTEGER, " -001 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_POSITIVE_INTEGER, " 000 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_POSITIVE_INTEGER, " 001 ", EXESS_SUCCESS, 2, "1");
+ check_write(EXESS_POSITIVE_INTEGER, " +001 ", EXESS_SUCCESS, 2, "1");
+
+ check_write(EXESS_POSITIVE_INTEGER,
+ " 36893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_POSITIVE_INTEGER,
+ " 0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+}
+
+static void
+test_fixed_numbers(void)
+{
+ check_write(EXESS_DOUBLE, " +00.10 ", EXESS_SUCCESS, 7, "1.0E-1");
+ check_write(EXESS_FLOAT, " +00.10 ", EXESS_SUCCESS, 14, "1.00000001E-1");
+ check_write(EXESS_BOOLEAN, " 0 ", EXESS_SUCCESS, 6, "false");
+ check_write(EXESS_BOOLEAN, " 1 ", EXESS_SUCCESS, 5, "true");
+ check_write(EXESS_LONG, " +012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_INT, " +012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_SHORT, " +012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_BYTE, " +012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_ULONG, " 012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_UINT, " 012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_USHORT, " 012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_UBYTE, " 012 ", EXESS_SUCCESS, 3, "12");
+}
+
+static void
+test_time(void)
+{
+ check_write(EXESS_DURATION, " P0Y6M ", EXESS_SUCCESS, 4, "P6M");
+ check_write(EXESS_DURATION, " P1Y6M0D ", EXESS_SUCCESS, 6, "P1Y6M");
+ check_write(EXESS_TIME, " 12:15:01+00:00 ", EXESS_SUCCESS, 14, "12:15:01Z");
+ check_write(
+ EXESS_DATE, " 02004-04-12+00:00 ", EXESS_SUCCESS, 12, "2004-04-12Z");
+}
+
+static void
+test_datetime(void)
+{
+ // Local
+ check_write(EXESS_DATETIME,
+ " 02001-02-03T04:05:06.007 ",
+ EXESS_SUCCESS,
+ 26,
+ "2001-02-03T04:05:06.007");
+
+ // Positive carry: minute => hour
+ check_write(EXESS_DATETIME,
+ " 02001-02-03T04:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-03T05:01:59Z");
+
+ // Positive carry: minute => hour => day
+ check_write(EXESS_DATETIME,
+ " 02001-02-03T23:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-04T00:01:59Z");
+
+ // Positive carry: minute => hour => day => month (common year)
+ check_write(EXESS_DATETIME,
+ " 02001-02-28T23:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-03-01T00:01:59Z");
+
+ // Positive carry: minute => hour => day => month (leap year)
+ check_write(EXESS_DATETIME,
+ " 02000-02-29T23:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2000-03-01T00:01:59Z");
+
+ // Positive carry: minute => hour => day => month => year
+ check_write(EXESS_DATETIME,
+ " 02001-12-31T23:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2002-01-01T00:01:59Z");
+
+ // Negative carry: minute => hour
+ check_write(EXESS_DATETIME,
+ " 02001-02-03T04:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-03T03:59:59Z");
+
+ // Negative carry: minute => hour => day
+ check_write(EXESS_DATETIME,
+ " 02001-02-02T00:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-01T23:59:59Z");
+
+ // Negative carry: minute => hour => day => month (common year)
+ check_write(EXESS_DATETIME,
+ " 02001-03-01T00:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-28T23:59:59Z");
+
+ // Negative carry: minute => hour => day => month (leap year)
+ check_write(EXESS_DATETIME,
+ " 02000-03-01T00:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2000-02-29T23:59:59Z");
+
+ // Negative carry: minute => hour => day => month => year
+ check_write(EXESS_DATETIME,
+ " 02001-01-01T00:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2000-12-31T23:59:59Z");
+}
+
+static void
+test_binary(void)
+{
+ check_write(EXESS_HEX, " D EA D B3 3F", EXESS_SUCCESS, 9, "DEADB33F");
+ check_write(EXESS_HEX, "invalid", EXESS_EXPECTED_HEX, 1, "");
+ check_write(EXESS_HEX, "1A2B3", EXESS_EXPECTED_HEX, 5, "");
+ check_write(EXESS_HEX, "1", EXESS_EXPECTED_HEX, 1, "");
+ check_write(EXESS_HEX, "", EXESS_EXPECTED_HEX, 5, "");
+
+ check_write(
+ EXESS_BASE64, " Z\fm\n9\rv\tY\vmFy", EXESS_SUCCESS, 9, "Zm9vYmFy");
+ check_write(EXESS_BASE64, "!nvalid", EXESS_EXPECTED_BASE64, 1, "");
+ check_write(EXESS_BASE64, "Z", EXESS_EXPECTED_BASE64, 1, "");
+ check_write(EXESS_BASE64, "Zm", EXESS_EXPECTED_BASE64, 2, "");
+ check_write(EXESS_BASE64, "Zm9", EXESS_EXPECTED_BASE64, 3, "");
+ check_write(EXESS_BASE64, "", EXESS_EXPECTED_BASE64, 5, "");
+}
+
+int
+main(void)
+{
+ check_write(EXESS_NOTHING, "?", EXESS_UNSUPPORTED, 1, "");
+
+ test_decimal();
+ test_integer();
+ test_fixed_numbers();
+ test_time();
+ test_datetime();
+ test_binary();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_coerce.c b/subprojects/exess/test/test_coerce.c
new file mode 100644
index 00000000..dc5ad060
--- /dev/null
+++ b/subprojects/exess/test/test_coerce.c
@@ -0,0 +1,519 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <string.h>
+
+static const char* const min_long_str = "-9223372036854775808";
+static const char* const max_long_str = "9223372036854775807";
+static const char* const max_ulong_str = "18446744073709551615";
+
+static void
+check_from_to(const ExessDatatype from_datatype,
+ const char* const from_string,
+ const ExessDatatype to_datatype,
+ const char* const to_string)
+{
+ char value_buf[4] = {0, 0, 0, 0};
+ char buf[328] = {42};
+
+ // Read original value
+ ExessVariant value = {EXESS_HEX, {.as_blob = {sizeof(value_buf), value_buf}}};
+ assert(!exess_read_variant(&value, from_datatype, from_string).status);
+
+ // Coerce to target datatype
+ const ExessVariant coerced = exess_coerce(value, to_datatype, EXESS_LOSSLESS);
+ assert(coerced.datatype == to_datatype);
+
+ // Write coerced value and check string against expectation
+ assert(!exess_write_variant(coerced, sizeof(buf), buf).status);
+ assert(!strcmp(buf, to_string));
+
+ // Coerce the value back to the original type
+ const ExessVariant tripped =
+ exess_coerce(coerced, from_datatype, EXESS_LOSSLESS);
+ assert(tripped.datatype == from_datatype);
+
+ // Write round-tripped value and check string against the original
+ assert(!exess_write_variant(tripped, sizeof(buf), buf).status);
+ assert(!strcmp(buf, from_string));
+}
+
+static void
+check_one_way(const ExessDatatype from_datatype,
+ const char* const from_string,
+ const ExessCoercionFlags coercions,
+ const ExessDatatype to_datatype,
+ const char* const to_string)
+{
+ char buf[328] = {42};
+
+ // Read original value
+ ExessVariant value = {EXESS_NOTHING, {EXESS_SUCCESS}};
+ assert(!exess_read_variant(&value, from_datatype, from_string).status);
+
+ // Coerce to target datatype
+ ExessVariant coerced = exess_coerce(value, to_datatype, coercions);
+ assert(coerced.datatype == to_datatype);
+
+ // Write coerced value and check string against expectation
+ assert(!exess_write_variant(coerced, sizeof(buf), buf).status);
+ assert(!strcmp(buf, to_string));
+}
+
+static void
+check_failure(const ExessDatatype from_datatype,
+ const char* const from_string,
+ const ExessDatatype to_datatype,
+ const ExessStatus expected_status)
+{
+ // Read original value
+ ExessVariant value = {EXESS_NOTHING, {EXESS_SUCCESS}};
+ assert(!exess_read_variant(&value, from_datatype, from_string).status);
+
+ // Try to coerce to target datatype
+ const ExessVariant coerced = exess_coerce(value, to_datatype, EXESS_LOSSLESS);
+ assert(coerced.datatype == EXESS_NOTHING);
+ assert(exess_get_status(&coerced) == expected_status);
+}
+
+static void
+test_unknown(void)
+{
+ ExessVariant long_value = exess_make_long(1);
+ ExessVariant ulong_value = exess_make_ulong(1u);
+ ExessVariant unknown_value = exess_make_nothing(EXESS_SUCCESS);
+
+ assert(exess_coerce(unknown_value, EXESS_LONG, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+
+ assert(exess_coerce(unknown_value, EXESS_ULONG, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+
+ assert(exess_coerce(long_value, EXESS_NOTHING, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+
+ assert(exess_coerce(ulong_value, EXESS_NOTHING, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+}
+
+static void
+test_boolean(void)
+{
+ // Exactly from 0 or 1, lossy from 0 or non-zero
+ check_from_to(EXESS_BOOLEAN, "false", EXESS_FLOAT, "0.0E0");
+ check_from_to(EXESS_BOOLEAN, "true", EXESS_FLOAT, "1.0E0");
+ check_from_to(EXESS_BOOLEAN, "false", EXESS_DOUBLE, "0.0E0");
+ check_from_to(EXESS_BOOLEAN, "true", EXESS_DOUBLE, "1.0E0");
+ check_from_to(EXESS_BOOLEAN, "false", EXESS_LONG, "0");
+ check_from_to(EXESS_BOOLEAN, "true", EXESS_LONG, "1");
+ check_failure(EXESS_LONG, "-1", EXESS_BOOLEAN, EXESS_WOULD_TRUNCATE);
+ check_failure(EXESS_LONG, "2", EXESS_BOOLEAN, EXESS_WOULD_TRUNCATE);
+ check_one_way(EXESS_LONG, "42", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+ check_one_way(EXESS_LONG, "-1", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+ check_from_to(EXESS_BOOLEAN, "false", EXESS_ULONG, "0");
+ check_from_to(EXESS_BOOLEAN, "true", EXESS_ULONG, "1");
+ check_failure(EXESS_LONG, "2", EXESS_BOOLEAN, EXESS_WOULD_TRUNCATE);
+ check_one_way(EXESS_ULONG, "42", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+
+ // Not convertible to any time types
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DATE, EXESS_UNSUPPORTED);
+}
+
+static void
+test_long(void)
+{
+ // Truncating boolean conversion
+ check_one_way(EXESS_LONG, "42", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+ check_one_way(EXESS_LONG, "-1", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+
+ // All smaller integer types
+ check_from_to(EXESS_LONG, "-2147483648", EXESS_INT, "-2147483648");
+ check_failure(EXESS_LONG, "-2147483649", EXESS_INT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "-32768", EXESS_SHORT, "-32768");
+ check_failure(EXESS_LONG, "-32769", EXESS_SHORT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "-128", EXESS_BYTE, "-128");
+ check_failure(EXESS_LONG, "-129", EXESS_BYTE, EXESS_OUT_OF_RANGE);
+
+ // Positive values to/from all unsigned types
+ check_from_to(EXESS_LONG, max_long_str, EXESS_ULONG, max_long_str);
+ check_failure(EXESS_LONG, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "4294967295", EXESS_UINT, "4294967295");
+ check_failure(EXESS_LONG, "-1", EXESS_UINT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "65535", EXESS_USHORT, "65535");
+ check_failure(EXESS_LONG, "-1", EXESS_USHORT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "255", EXESS_UBYTE, "255");
+ check_failure(EXESS_LONG, "-1", EXESS_UBYTE, EXESS_OUT_OF_RANGE);
+
+ // Any value to/from integer
+ check_from_to(EXESS_LONG, min_long_str, EXESS_INTEGER, min_long_str);
+ check_from_to(EXESS_LONG, max_long_str, EXESS_INTEGER, max_long_str);
+
+ // Non-positive values to/from nonPositiveInteger
+ check_from_to(
+ EXESS_LONG, min_long_str, EXESS_NON_POSITIVE_INTEGER, min_long_str);
+ check_from_to(EXESS_LONG, "0", EXESS_NON_POSITIVE_INTEGER, "0");
+ check_failure(
+ EXESS_LONG, "1", EXESS_NON_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Negative values to/from negativeInteger
+ check_from_to(EXESS_LONG, min_long_str, EXESS_NEGATIVE_INTEGER, min_long_str);
+ check_from_to(EXESS_LONG, "-1", EXESS_NEGATIVE_INTEGER, "-1");
+ check_failure(EXESS_LONG, "0", EXESS_NEGATIVE_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Non-negative values to/from nonNegativeInteger
+ check_failure(
+ EXESS_LONG, "-1", EXESS_NON_NEGATIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "0", EXESS_NON_NEGATIVE_INTEGER, "0");
+ check_from_to(
+ EXESS_LONG, max_long_str, EXESS_NON_NEGATIVE_INTEGER, max_long_str);
+
+ // Positive values to/from positiveInteger
+ check_failure(EXESS_LONG, "-1", EXESS_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_LONG, "0", EXESS_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, max_long_str, EXESS_POSITIVE_INTEGER, max_long_str);
+ check_failure(EXESS_POSITIVE_INTEGER,
+ "9223372036854775808",
+ EXESS_LONG,
+ EXESS_OUT_OF_RANGE);
+
+ // Float
+ check_failure(EXESS_FLOAT, "1.5", EXESS_LONG, EXESS_WOULD_ROUND);
+ check_from_to(EXESS_LONG, "-16777215", EXESS_FLOAT, "-1.6777215E7");
+ check_failure(EXESS_LONG, "-16777216", EXESS_FLOAT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_FLOAT, "-16777216", EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "16777215", EXESS_FLOAT, "1.6777215E7");
+ check_failure(EXESS_LONG, "16777216", EXESS_FLOAT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_FLOAT, "16777216", EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_one_way(EXESS_FLOAT, "1.0", EXESS_LOSSLESS, EXESS_LONG, "1");
+ check_one_way(EXESS_FLOAT, "1.5", EXESS_ROUND, EXESS_LONG, "2");
+ check_one_way(EXESS_FLOAT, "2.5", EXESS_ROUND, EXESS_LONG, "2");
+ check_one_way(EXESS_FLOAT, "3.5", EXESS_ROUND, EXESS_LONG, "4");
+
+ // Double
+ check_failure(EXESS_DOUBLE, "1.5", EXESS_LONG, EXESS_WOULD_ROUND);
+ check_from_to(
+ EXESS_LONG, "-9007199254740991", EXESS_DOUBLE, "-9.007199254740991E15");
+ check_failure(
+ EXESS_LONG, "-9007199254740992", EXESS_DOUBLE, EXESS_OUT_OF_RANGE);
+ check_failure(
+ EXESS_DOUBLE, "-9007199254740992", EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_from_to(
+ EXESS_LONG, "9007199254740991", EXESS_DOUBLE, "9.007199254740991E15");
+ check_failure(
+ EXESS_LONG, "9007199254740992", EXESS_DOUBLE, EXESS_OUT_OF_RANGE);
+ check_failure(
+ EXESS_DOUBLE, "9007199254740992", EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_one_way(EXESS_DOUBLE, "1.0", EXESS_LOSSLESS, EXESS_LONG, "1");
+ check_one_way(EXESS_DOUBLE, "1.5", EXESS_ROUND, EXESS_LONG, "2");
+ check_one_way(EXESS_DOUBLE, "2.5", EXESS_ROUND, EXESS_LONG, "2");
+ check_one_way(EXESS_DOUBLE, "3.5", EXESS_ROUND, EXESS_LONG, "4");
+}
+
+static void
+test_ulong(void)
+{
+ ExessVariant unknown = {EXESS_NOTHING, {EXESS_SUCCESS}};
+
+ assert(exess_coerce(unknown, EXESS_ULONG, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+
+ // Truncating boolean conversion
+ check_one_way(EXESS_ULONG, "42", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+
+ // All integer types
+ check_from_to(EXESS_ULONG, max_long_str, EXESS_LONG, max_long_str);
+ check_failure(EXESS_ULONG, max_ulong_str, EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "2147483647", EXESS_INT, "2147483647");
+ check_failure(EXESS_ULONG, "2147483648", EXESS_INT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_INT, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "32767", EXESS_SHORT, "32767");
+ check_failure(EXESS_ULONG, "32768", EXESS_SHORT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_SHORT, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "127", EXESS_BYTE, "127");
+ check_failure(EXESS_ULONG, "128", EXESS_BYTE, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_BYTE, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+
+ // All unsigned types
+ check_from_to(EXESS_ULONG, "4294967295", EXESS_UINT, "4294967295");
+ check_failure(EXESS_ULONG, "4294967296", EXESS_UINT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "65535", EXESS_USHORT, "65535");
+ check_failure(EXESS_ULONG, "65536", EXESS_USHORT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "255", EXESS_UBYTE, "255");
+ check_failure(EXESS_ULONG, "256", EXESS_UBYTE, EXESS_OUT_OF_RANGE);
+
+ // Small enough value Any value to/from integer
+ check_from_to(EXESS_ULONG, "0", EXESS_INTEGER, "0");
+ check_failure(
+ EXESS_ULONG, "9223372036854775808", EXESS_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Only zero to/from nonPositiveInteger
+ check_from_to(EXESS_ULONG, "0", EXESS_NON_POSITIVE_INTEGER, "0");
+ check_failure(
+ EXESS_ULONG, "1", EXESS_NON_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Not convertible to/from negativeInteger
+ check_failure(EXESS_ULONG, "0", EXESS_NEGATIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_ULONG, "1", EXESS_NEGATIVE_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Any value to/from nonNegativeInteger
+ check_from_to(EXESS_ULONG, "0", EXESS_NON_NEGATIVE_INTEGER, "0");
+ check_from_to(
+ EXESS_ULONG, max_ulong_str, EXESS_NON_NEGATIVE_INTEGER, max_ulong_str);
+
+ // Positive values to/from positiveInteger
+ check_failure(EXESS_ULONG, "0", EXESS_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "1", EXESS_POSITIVE_INTEGER, "1");
+
+ // Float
+ check_failure(EXESS_FLOAT, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_FLOAT, "1.5", EXESS_ULONG, EXESS_WOULD_ROUND);
+ check_from_to(EXESS_ULONG, "0", EXESS_FLOAT, "0.0E0");
+ check_from_to(EXESS_ULONG, "16777215", EXESS_FLOAT, "1.6777215E7");
+ check_failure(EXESS_ULONG, "16777216", EXESS_FLOAT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_FLOAT, "16777216", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_one_way(EXESS_FLOAT, "1.0", EXESS_LOSSLESS, EXESS_ULONG, "1");
+ check_one_way(EXESS_FLOAT, "1.5", EXESS_ROUND, EXESS_ULONG, "2");
+ check_one_way(EXESS_FLOAT, "2.5", EXESS_ROUND, EXESS_ULONG, "2");
+ check_one_way(EXESS_FLOAT, "3.5", EXESS_ROUND, EXESS_ULONG, "4");
+
+ // Double
+ check_failure(EXESS_DOUBLE, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_DOUBLE, "1.5", EXESS_ULONG, EXESS_WOULD_ROUND);
+ check_from_to(EXESS_ULONG, "0", EXESS_DOUBLE, "0.0E0");
+ check_from_to(
+ EXESS_ULONG, "9007199254740991", EXESS_DOUBLE, "9.007199254740991E15");
+ check_failure(
+ EXESS_ULONG, "9007199254740992", EXESS_DOUBLE, EXESS_OUT_OF_RANGE);
+ check_failure(
+ EXESS_DOUBLE, "9007199254740992", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_one_way(EXESS_DOUBLE, "1.0", EXESS_LOSSLESS, EXESS_ULONG, "1");
+ check_one_way(EXESS_DOUBLE, "1.5", EXESS_ROUND, EXESS_ULONG, "2");
+ check_one_way(EXESS_DOUBLE, "2.5", EXESS_ROUND, EXESS_ULONG, "2");
+ check_one_way(EXESS_DOUBLE, "3.5", EXESS_ROUND, EXESS_ULONG, "4");
+}
+
+static void
+test_large_integers(void)
+{
+ check_failure(EXESS_TIME, "00:00:00", EXESS_INTEGER, EXESS_UNSUPPORTED);
+ check_failure(
+ EXESS_TIME, "00:00:00", EXESS_NON_POSITIVE_INTEGER, EXESS_UNSUPPORTED);
+ check_failure(
+ EXESS_TIME, "00:00:00", EXESS_NEGATIVE_INTEGER, EXESS_UNSUPPORTED);
+ check_failure(
+ EXESS_TIME, "00:00:00", EXESS_NON_NEGATIVE_INTEGER, EXESS_UNSUPPORTED);
+ check_failure(
+ EXESS_TIME, "00:00:00", EXESS_POSITIVE_INTEGER, EXESS_UNSUPPORTED);
+}
+
+static void
+test_coerce(void)
+{
+ check_one_way(EXESS_DOUBLE,
+ "1.0000000000001",
+ EXESS_REDUCE_PRECISION,
+ EXESS_FLOAT,
+ "1.0E0");
+
+ check_failure(
+ EXESS_DOUBLE, "1.0000000000001", EXESS_FLOAT, EXESS_WOULD_REDUCE_PRECISION);
+
+ check_one_way(EXESS_FLOAT, "1.5", EXESS_LOSSLESS, EXESS_DOUBLE, "1.5E0");
+
+ /* check_failure( */
+ /* EXESS_LONG, "9007199254740993", EXESS_DOUBLE, EXESS_OUT_OF_RANGE); */
+
+ /* check_failure(EXESS_FLOAT, "1.0", EXESS_LONG, EXESS_SUCCESS); */
+ /* check_from_to(EXESS_BYTE, "-128", EXESS_LONG, "-128"); */
+
+ // DateTime
+}
+
+static void
+test_date_time(void)
+{
+ check_failure(
+ EXESS_DATETIME, "2001-02-03T04:05:06", EXESS_TIME, EXESS_WOULD_TRUNCATE);
+
+ check_one_way(EXESS_DATETIME,
+ "2001-02-03T04:05:06",
+ EXESS_TRUNCATE,
+ EXESS_TIME,
+ "04:05:06");
+
+ check_one_way(EXESS_DATETIME,
+ "2001-02-03T04:05:06Z",
+ EXESS_TRUNCATE,
+ EXESS_TIME,
+ "04:05:06Z");
+
+ check_failure(
+ EXESS_DATETIME, "2001-02-03T04:05:06", EXESS_DATE, EXESS_WOULD_TRUNCATE);
+
+ check_one_way(EXESS_DATETIME,
+ "2001-02-03T04:05:06",
+ EXESS_TRUNCATE,
+ EXESS_DATE,
+ "2001-02-03");
+
+ check_one_way(EXESS_DATETIME,
+ "2001-02-03T04:05:06Z",
+ EXESS_TRUNCATE,
+ EXESS_DATE,
+ "2001-02-03Z");
+}
+
+static void
+test_number_to_time(void)
+{
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_LONG, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_LONG, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_LONG, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_LONG, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_INT, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_INT, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_INT, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_INT, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_SHORT, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_SHORT, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_SHORT, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_SHORT, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_BYTE, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BYTE, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BYTE, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BYTE, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_ULONG, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_ULONG, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_ULONG, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_ULONG, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_UINT, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UINT, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UINT, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UINT, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_USHORT, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_USHORT, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_USHORT, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_USHORT, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_UBYTE, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UBYTE, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UBYTE, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UBYTE, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+}
+
+static void
+test_time_to_number(void)
+{
+ static const char* const duration_str = "P1Y";
+ static const char* const datetime_str = "2001-02-03T04:05:06";
+ static const char* const time_str = "04:05:06";
+ static const char* const date_str = "2001-02-03";
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_BOOLEAN, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_BOOLEAN, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_BOOLEAN, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_BOOLEAN, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_INT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_INT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_INT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_INT, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_SHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_SHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_SHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_SHORT, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_BYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_BYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_BYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_BYTE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_ULONG, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_ULONG, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_ULONG, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_ULONG, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_UINT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_UINT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_UINT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_UINT, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_USHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_USHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_USHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_USHORT, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_UBYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_UBYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_UBYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_UBYTE, EXESS_UNSUPPORTED);
+}
+
+static void
+test_binary(void)
+{
+ check_from_to(EXESS_HEX, "666F6F", EXESS_BASE64, "Zm9v");
+
+ check_failure(EXESS_LONG, "-2147483649", EXESS_HEX, EXESS_UNSUPPORTED);
+ check_failure(EXESS_LONG, "-2147483649", EXESS_BASE64, EXESS_UNSUPPORTED);
+
+ /* check_from_to(EXESS_BASE64, "Zm9v", EXESS_HEX, "666F6F"); */
+
+ /* ////////// */
+
+ /* check_one_way(EXESS_LONG, "-1", EXESS_TRUNCATE, EXESS_BOOLEAN, "true"); */
+
+ /* // All smaller integer types */
+ /* check_from_to(EXESS_LONG, "-2147483648", EXESS_INT, "-2147483648"); */
+ /* check_failure(EXESS_LONG, "-2147483649", EXESS_INT, EXESS_OUT_OF_RANGE); */
+}
+
+int
+main(void)
+{
+ test_unknown();
+ test_boolean();
+ test_long();
+ test_ulong();
+ test_large_integers();
+ test_coerce();
+ test_date_time();
+ test_number_to_time();
+ test_time_to_number();
+ test_binary();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_datatype.c b/subprojects/exess/test/test_datatype.c
new file mode 100644
index 00000000..7ad3fb5e
--- /dev/null
+++ b/subprojects/exess/test/test_datatype.c
@@ -0,0 +1,81 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+
+static void
+test_datatype_uris(void)
+{
+ assert(!exess_datatype_uri(EXESS_NOTHING));
+ assert(!exess_datatype_uri((ExessDatatype)(EXESS_BASE64 + 1)));
+
+ for (unsigned i = 1; i <= EXESS_BASE64; ++i) {
+ const char* const uri = exess_datatype_uri((ExessDatatype)i);
+
+ assert(uri);
+ assert(exess_datatype_from_uri(uri) == i);
+ }
+
+ assert(exess_datatype_from_uri(EXESS_XSD_URI) == EXESS_NOTHING);
+ assert(exess_datatype_from_uri(EXESS_XSD_URI "unknown") == EXESS_NOTHING);
+ assert(exess_datatype_from_uri("garbage") == EXESS_NOTHING);
+ assert(exess_datatype_from_uri("http://example.org/very/long/unknown/uri") ==
+ EXESS_NOTHING);
+}
+
+static void
+test_datatype_is_bounded(void)
+{
+ assert(!exess_datatype_is_bounded(EXESS_NOTHING));
+ assert(exess_datatype_is_bounded(EXESS_BOOLEAN));
+ assert(!exess_datatype_is_bounded(EXESS_DECIMAL));
+ assert(exess_datatype_is_bounded(EXESS_DOUBLE));
+ assert(exess_datatype_is_bounded(EXESS_FLOAT));
+ assert(!exess_datatype_is_bounded(EXESS_INTEGER));
+ assert(!exess_datatype_is_bounded(EXESS_NON_POSITIVE_INTEGER));
+ assert(!exess_datatype_is_bounded(EXESS_NEGATIVE_INTEGER));
+ assert(exess_datatype_is_bounded(EXESS_LONG));
+ assert(exess_datatype_is_bounded(EXESS_INT));
+ assert(exess_datatype_is_bounded(EXESS_SHORT));
+ assert(exess_datatype_is_bounded(EXESS_BYTE));
+ assert(!exess_datatype_is_bounded(EXESS_NON_NEGATIVE_INTEGER));
+ assert(exess_datatype_is_bounded(EXESS_ULONG));
+ assert(exess_datatype_is_bounded(EXESS_UINT));
+ assert(exess_datatype_is_bounded(EXESS_USHORT));
+ assert(exess_datatype_is_bounded(EXESS_UBYTE));
+ assert(!exess_datatype_is_bounded(EXESS_POSITIVE_INTEGER));
+ assert(exess_datatype_is_bounded(EXESS_DURATION));
+ assert(exess_datatype_is_bounded(EXESS_DATETIME));
+ assert(exess_datatype_is_bounded(EXESS_TIME));
+ assert(exess_datatype_is_bounded(EXESS_DATE));
+ assert(!exess_datatype_is_bounded(EXESS_HEX));
+ assert(!exess_datatype_is_bounded(EXESS_BASE64));
+}
+
+int
+main(int argc, char** argv)
+{
+ (void)argv;
+
+ test_datatype_uris();
+ test_datatype_is_bounded();
+
+ return argc == 1 ? 0 : 1;
+}
diff --git a/subprojects/exess/test/test_date.c b/subprojects/exess/test/test_date.c
new file mode 100644
index 00000000..0ac8129a
--- /dev/null
+++ b/subprojects/exess/test/test_date.c
@@ -0,0 +1,257 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "date_utils.h"
+#include "int_test_data.h"
+#include "macros.h"
+#include "num_test_utils.h"
+#include "time_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static const ExessDate nozone = {2001, 1, 2, {EXESS_LOCAL}};
+static const ExessDate utc = {2002, 2, 3, {0}};
+static const ExessDate zoned = {2003, 3, 4, INIT_ZONE(11, 30)};
+static const ExessDate early = {99, 3, 4, INIT_ZONE(11, 30)};
+static const ExessDate future = {12345, 3, 4, INIT_ZONE(11, 30)};
+static const ExessDate lowest = {INT16_MIN, 1, 1, INIT_ZONE(-14, 0)};
+static const ExessDate highest = {INT16_MAX, 1, 1, INIT_ZONE(14, 0)};
+static const ExessDate garbage1 = {2004, 0, 1, INIT_ZONE(11, 30)};
+static const ExessDate garbage2 = {2005, 13, 1, INIT_ZONE(11, 30)};
+static const ExessDate garbage3 = {2006, 1, 0, INIT_ZONE(11, 30)};
+static const ExessDate garbage4 = {2006, 1, 32, INIT_ZONE(11, 30)};
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int64_t expected_year,
+ const uint8_t expected_month,
+ const uint8_t expected_day,
+ const int8_t expected_tz_hour,
+ const int8_t expected_tz_minute,
+ const bool expected_tz_is_present,
+ const size_t expected_count)
+{
+ ExessDate value = {0, 0, 0, {EXESS_LOCAL}};
+
+ const ExessResult r = exess_read_date(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value.year == expected_year);
+ assert(value.month == expected_month);
+ assert(value.day == expected_day);
+ assert((!expected_tz_is_present && value.zone.quarter_hours == EXESS_LOCAL) ||
+ value.zone.quarter_hours ==
+ 4 * expected_tz_hour + expected_tz_minute / 15);
+}
+
+static void
+test_read_date(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, false, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, false, 6);
+
+ // Good values
+ check_read("2004-04-12", EXESS_SUCCESS, 2004, 4, 12, 0, 0, false, 10);
+ check_read("-0045-01-01", EXESS_SUCCESS, -45, 1, 1, 0, 0, false, 11);
+ check_read("12004-04-12", EXESS_SUCCESS, 12004, 4, 12, 0, 0, false, 11);
+ check_read("2004-04-12-05:00", EXESS_SUCCESS, 2004, 4, 12, -5, 0, true, 16);
+ check_read("2004-04-12Z", EXESS_SUCCESS, 2004, 4, 12, 0, 0, true, 11);
+ check_read("2001-10-26", EXESS_SUCCESS, 2001, 10, 26, 0, 0, false, 10);
+ check_read("2001-10-26+02:00", EXESS_SUCCESS, 2001, 10, 26, 2, 0, true, 16);
+ check_read("2001-10-26Z", EXESS_SUCCESS, 2001, 10, 26, 0, 0, true, 11);
+ check_read("2001-10-26+00:00", EXESS_SUCCESS, 2001, 10, 26, 0, 0, true, 16);
+ check_read("-2001-10-26", EXESS_SUCCESS, -2001, 10, 26, 0, 0, false, 11);
+ check_read("-20000-04-01", EXESS_SUCCESS, -20000, 04, 01, 0, 0, false, 12);
+
+ // Non-canonical
+ check_read("02004-04-12", EXESS_SUCCESS, 2004, 4, 12, 0, 0, false, 11);
+ check_read(" 02004-04-12 ", EXESS_SUCCESS, 2004, 4, 12, 0, 0, false, 12);
+
+ // Good common year values
+ check_read("1900-02-28", EXESS_SUCCESS, 1900, 2, 28, 0, 0, false, 10);
+
+ // Good leap year values
+ check_read("2000-02-29", EXESS_SUCCESS, 2000, 2, 29, 0, 0, false, 10);
+ check_read("2004-02-29", EXESS_SUCCESS, 2004, 2, 29, 0, 0, false, 10);
+
+ // Longest possible string
+ check_read("-32768-01-01-14:00",
+ EXESS_SUCCESS,
+ -32768,
+ 1,
+ 1,
+ -14,
+ 0,
+ true,
+ EXESS_MAX_DATE_LENGTH);
+
+ // Limits
+ check_read("-32768-01-01", EXESS_SUCCESS, -32768, 1, 1, 0, 0, false, 12);
+ check_read("32767-01-01", EXESS_SUCCESS, 32767, 1, 1, 0, 0, false, 11);
+
+ // Out of range years
+ check_read("-32769-01-01", EXESS_OUT_OF_RANGE, 0, 0, 0, 0, 0, false, 6);
+ check_read("32768-01-01", EXESS_OUT_OF_RANGE, 0, 0, 0, 0, 0, false, 5);
+
+ // Out of range months
+ check_read("2001-00-26", EXESS_OUT_OF_RANGE, 2001, 0, 0, 0, 0, false, 7);
+ check_read("2001-13-26", EXESS_OUT_OF_RANGE, 2001, 13, 0, 0, 0, false, 7);
+ check_read("2001-10-00", EXESS_OUT_OF_RANGE, 2001, 10, 0, 0, 0, false, 10);
+ check_read("2001-10-32", EXESS_OUT_OF_RANGE, 2001, 10, 32, 0, 0, false, 10);
+
+ // Out of range days
+ check_read("2001-01-32", EXESS_OUT_OF_RANGE, 2001, 1, 32, 0, 0, false, 10);
+ check_read("2001-02-29", EXESS_OUT_OF_RANGE, 2001, 2, 29, 0, 0, false, 10);
+ check_read("2001-03-32", EXESS_OUT_OF_RANGE, 2001, 3, 32, 0, 0, false, 10);
+ check_read("2001-04-31", EXESS_OUT_OF_RANGE, 2001, 4, 31, 0, 0, false, 10);
+ check_read("2001-05-32", EXESS_OUT_OF_RANGE, 2001, 5, 32, 0, 0, false, 10);
+ check_read("2001-06-31", EXESS_OUT_OF_RANGE, 2001, 6, 31, 0, 0, false, 10);
+ check_read("2001-07-32", EXESS_OUT_OF_RANGE, 2001, 7, 32, 0, 0, false, 10);
+ check_read("2001-08-32", EXESS_OUT_OF_RANGE, 2001, 8, 32, 0, 0, false, 10);
+ check_read("2001-09-31", EXESS_OUT_OF_RANGE, 2001, 9, 31, 0, 0, false, 10);
+ check_read("2001-10-32", EXESS_OUT_OF_RANGE, 2001, 10, 32, 0, 0, false, 10);
+ check_read("2001-11-31", EXESS_OUT_OF_RANGE, 2001, 11, 31, 0, 0, false, 10);
+ check_read("2001-12-32", EXESS_OUT_OF_RANGE, 2001, 12, 32, 0, 0, false, 10);
+
+ // Garbage
+ check_read("f", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, false, 0);
+ check_read("99-04-12", EXESS_EXPECTED_DIGIT, 99, 0, 0, 0, 0, false, 2);
+ check_read("2004-4-2", EXESS_EXPECTED_DIGIT, 2004, 4, 0, 0, 0, false, 6);
+ check_read("2004/04/02", EXESS_EXPECTED_DASH, 2004, 0, 0, 0, 0, false, 4);
+ check_read("04-12-2004", EXESS_EXPECTED_DIGIT, 4, 0, 0, 0, 0, false, 2);
+ check_read("2001-10", EXESS_EXPECTED_DASH, 2001, 10, 0, 0, 0, false, 7);
+ check_read("01-10-26", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, false, 2);
+ check_read("2004-04-12A", EXESS_EXPECTED_SIGN, 2004, 4, 12, 0, 0, false, 10);
+}
+
+static void
+check_write(const ExessDate value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DATE_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_date(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_date(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_date(void)
+{
+ check_write(nozone, EXESS_SUCCESS, 11, "2001-01-02");
+ check_write(utc, EXESS_SUCCESS, 12, "2002-02-03Z");
+ check_write(zoned, EXESS_SUCCESS, 17, "2003-03-04+11:30");
+ check_write(early, EXESS_SUCCESS, 17, "0099-03-04+11:30");
+ check_write(future, EXESS_SUCCESS, 18, "12345-03-04+11:30");
+ check_write(lowest, EXESS_SUCCESS, 19, "-32768-01-01-14:00");
+ check_write(highest, EXESS_SUCCESS, 18, "32767-01-01+14:00");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 14, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 14, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 14, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 14, "");
+
+ check_write(nozone, EXESS_NO_SPACE, 10, "");
+ check_write(future, EXESS_NO_SPACE, 5, "");
+ check_write(lowest, EXESS_NO_SPACE, 5, "");
+ check_write(highest, EXESS_NO_SPACE, 5, "");
+ check_write(utc, EXESS_NO_SPACE, 11, "");
+ check_write(zoned, EXESS_NO_SPACE, 16, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_date(nozone, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+static void
+check_round_trip(const ExessDate value)
+{
+ ExessDate parsed_value = {0, 0, 0, {0}};
+ char buf[EXESS_MAX_DATE_LENGTH + 1] = {0};
+
+ assert(!exess_write_date(value, sizeof(buf), buf).status);
+ assert(!exess_read_date(&parsed_value, buf).status);
+ assert(parsed_value.year == value.year);
+ assert(parsed_value.month == value.month);
+ assert(parsed_value.day == value.day);
+ assert(parsed_value.zone.quarter_hours == value.zone.quarter_hours);
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ fprintf(stderr, "Testing xsd:gDate randomly with seed %u\n", opts.seed);
+
+ const uint64_t n_tests = MAX(256, opts.n_tests / 16);
+
+ uint32_t rng = opts.seed;
+ for (uint64_t i = 0; i < n_tests; ++i) {
+ rng = lcg32(rng);
+
+ const int16_t year = (int16_t)(rng % UINT16_MAX);
+ for (uint8_t month = 1; month < 13; ++month) {
+ for (uint8_t day = 1; day <= days_in_month(year, month); ++day) {
+ const ExessDate no_zone = {year, month, day, {EXESS_LOCAL}};
+ const ExessDate lowest_zone = {year, month, day, {4 * -14 + 0}};
+ const ExessDate highest_zone = {year, month, day, {4 * 14}};
+
+ check_round_trip(no_zone);
+ check_round_trip(lowest_zone);
+ check_round_trip(highest_zone);
+
+ const ExessDate value = {year, month, day, random_timezone(&rng)};
+ check_round_trip(value);
+ }
+ }
+
+ print_num_test_progress(i, n_tests);
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_date();
+ test_write_date();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_datetime.c b/subprojects/exess/test/test_datetime.c
new file mode 100644
index 00000000..d6d2b378
--- /dev/null
+++ b/subprojects/exess/test/test_datetime.c
@@ -0,0 +1,459 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+static const ExessDateTime local = {2001, 2, 3, false, 4, 5, 6, 0};
+static const ExessDateTime utc = {2001, 2, 3, true, 4, 5, 6, 0};
+static const ExessDateTime lowest = {INT16_MIN, 1, 1, false, 0, 0, 0, 0};
+static const ExessDateTime highest = {INT16_MAX, 12, 31, false, 24, 0, 0, 0};
+static const ExessDateTime utc_min = {INT16_MIN, 1, 1, true, 0, 0, 0, 0};
+static const ExessDateTime utc_max = {INT16_MAX, 12, 31, true, 24, 0, 0, 0};
+static const ExessDateTime nano = {2001, 1, 1, false, 0, 0, 0, 1};
+static const ExessDateTime garbage1 = {2004, 0, 1, false, 12, 0, 0, 0};
+static const ExessDateTime garbage2 = {2005, 13, 1, false, 12, 0, 0, 0};
+static const ExessDateTime garbage3 = {2006, 1, 0, false, 12, 0, 0, 0};
+static const ExessDateTime garbage4 = {2006, 1, 32, false, 12, 0, 0, 0};
+static const ExessDateTime garbage5 = {2001, 2, 3, false, 0, 0, 0, 1000000000};
+static const ExessDateTime garbage6 = {2001, 2, 3, false, 0, 0, 60, 0};
+static const ExessDateTime garbage7 = {2001, 2, 3, false, 0, 60, 0, 0};
+static const ExessDateTime garbage8 = {2001, 2, 3, false, 24, 0, 0, 1};
+static const ExessDateTime garbage9 = {2001, 2, 3, false, 24, 0, 1, 0};
+static const ExessDateTime garbage10 = {2001, 2, 3, false, 24, 1, 0, 0};
+static const ExessDateTime garbage11 = {2001, 2, 3, false, 25, 0, 0, 0};
+
+static void
+check_add(const char* const datetime_string,
+ const char* const duration_string,
+ const char* const result_string)
+{
+ ExessDateTime datetime = {0, 0u, 0u, false, 0u, 0u, 0u, 0u};
+ ExessDuration duration = {0u, 0u, 0u};
+
+ ExessResult r = exess_read_datetime(&datetime, datetime_string);
+ assert(!r.status);
+
+ r = exess_read_duration(&duration, duration_string);
+ assert(!r.status);
+
+ const ExessDateTime result = exess_add_datetime_duration(datetime, duration);
+ char buf[28] = {0};
+
+ r = exess_write_datetime(result, sizeof(buf), buf);
+ assert(!r.status);
+ assert(!strcmp(buf, result_string));
+}
+
+static void
+check_is_underflow(const ExessDateTime datetime, const bool is_utc)
+{
+ assert(datetime.year == INT16_MIN);
+ assert(datetime.month == 0);
+ assert(datetime.day == 0);
+ assert(datetime.is_utc == is_utc);
+ assert(datetime.hour == 0);
+ assert(datetime.minute == 0);
+ assert(datetime.second == 0);
+ assert(datetime.nanosecond == 0);
+}
+
+static void
+check_is_overflow(const ExessDateTime datetime, const bool is_utc)
+{
+ assert(datetime.year == INT16_MAX);
+ assert(datetime.month == UINT8_MAX);
+ assert(datetime.day == UINT8_MAX);
+ assert(datetime.is_utc == is_utc);
+ assert(datetime.hour == UINT8_MAX);
+ assert(datetime.minute == UINT8_MAX);
+ assert(datetime.second == UINT8_MAX);
+ assert(datetime.nanosecond == UINT32_MAX);
+}
+
+static void
+test_add(void)
+{
+ // Simple cases
+ check_add("2001-01-01T00:00:00", "PT1.5S", "2001-01-01T00:00:01.5");
+ check_add("2001-01-01T00:00:00", "PT1M", "2001-01-01T00:01:00");
+ check_add("2001-01-01T00:00:00", "PT1H", "2001-01-01T01:00:00");
+ check_add("2001-01-01T00:00:00", "P1D", "2001-01-02T00:00:00");
+ check_add("2001-01-01T00:00:00", "P1M", "2001-02-01T00:00:00");
+ check_add("2001-01-01T00:00:00", "P1Y", "2002-01-01T00:00:00");
+ check_add("2001-02-02T02:02:02", "-PT1.5S", "2001-02-02T02:02:00.5");
+ check_add("2001-02-02T02:02:02", "-PT1M", "2001-02-02T02:01:02");
+ check_add("2001-02-02T02:02:02", "-PT1H", "2001-02-02T01:02:02");
+ check_add("2001-02-02T02:02:02", "-P1D", "2001-02-01T02:02:02");
+ check_add("2001-02-02T02:02:02", "-P1M", "2001-01-02T02:02:02");
+ check_add("2001-02-02T02:02:02", "-P1Y", "2000-02-02T02:02:02");
+
+ // Positive carrying
+ check_add("2001-01-01T00:00:59", "PT1S", "2001-01-01T00:01:00");
+ check_add("2001-01-01T00:59:00", "PT1M", "2001-01-01T01:00:00");
+ check_add("2001-01-01T23:00:00", "PT1H", "2001-01-02T00:00:00");
+ check_add("2001-01-31T00:00:00", "P1D", "2001-02-01T00:00:00");
+ check_add("2001-12-01T00:00:00", "P1M", "2002-01-01T00:00:00");
+
+ // Negative carrying
+ check_add("2001-01-01T00:01:00", "-PT1S", "2001-01-01T00:00:59");
+ check_add("2001-02-01T01:00:00", "-PT1M", "2001-02-01T00:59:00");
+ check_add("2001-02-02T00:00:00", "-PT1H", "2001-02-01T23:00:00");
+ check_add("2001-02-01T00:00:00", "-P1D", "2001-01-31T00:00:00");
+ check_add("2001-01-01T00:00:00", "-P1M", "2000-12-01T00:00:00");
+
+ // Underflow and overflow
+
+ static const ExessDuration minus_month = {-1, 0, 0};
+ static const ExessDuration minus_second = {0, -1, 0};
+ static const ExessDuration minus_nanosecond = {0, 0, -1};
+ static const ExessDuration plus_month = {1, 0, 0};
+ static const ExessDuration plus_second = {0, 1, 0};
+ static const ExessDuration plus_nanosecond = {0, 0, 1};
+
+ check_is_underflow(exess_add_datetime_duration(lowest, minus_month), false);
+ check_is_underflow(exess_add_datetime_duration(lowest, minus_second), false);
+ check_is_underflow(exess_add_datetime_duration(lowest, minus_nanosecond),
+ false);
+
+ check_is_underflow(exess_add_datetime_duration(utc_min, minus_month), true);
+ check_is_underflow(exess_add_datetime_duration(utc_min, minus_second), true);
+ check_is_underflow(exess_add_datetime_duration(utc_min, minus_nanosecond),
+ true);
+
+ check_is_overflow(exess_add_datetime_duration(highest, plus_month), false);
+ check_is_overflow(exess_add_datetime_duration(highest, plus_second), false);
+ check_is_overflow(exess_add_datetime_duration(highest, plus_nanosecond),
+ false);
+
+ check_is_overflow(exess_add_datetime_duration(utc_max, plus_month), true);
+ check_is_overflow(exess_add_datetime_duration(utc_max, plus_second), true);
+ check_is_overflow(exess_add_datetime_duration(utc_max, plus_nanosecond),
+ true);
+}
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_count,
+ const int64_t expected_year,
+ const uint8_t expected_month,
+ const uint8_t expected_day,
+ const uint8_t expected_hour,
+ const uint8_t expected_minute,
+ const uint8_t expected_second,
+ const uint32_t expected_nanosecond,
+ const bool expected_is_utc)
+{
+ ExessDateTime value = {0, 0, 0, false, 0, 0, 0, 0};
+
+ const ExessResult r = exess_read_datetime(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value.year == expected_year);
+ assert(value.month == expected_month);
+ assert(value.day == expected_day);
+ assert(value.hour == expected_hour);
+ assert(value.minute == expected_minute);
+ assert(value.second == expected_second);
+ assert(value.nanosecond == expected_nanosecond);
+ assert(value.is_utc == expected_is_utc);
+}
+
+static void
+test_read_datetime(void)
+{
+ // Simple values
+
+ check_read(
+ "2001-02-03T04:05:06", EXESS_SUCCESS, 19, 2001, 2, 3, 4, 5, 6, 0, false);
+
+ check_read(
+ "2001-02-03T04:05:06Z", EXESS_SUCCESS, 20, 2001, 2, 3, 4, 5, 6, 0, true);
+
+ check_read("2004-04-12T13:20:15.5",
+ EXESS_SUCCESS,
+ 21,
+ 2004,
+ 4,
+ 12,
+ 13,
+ 20,
+ 15,
+ 500000000,
+ false);
+
+ check_read("-32768-01-01T00:00:00.000000001Z",
+ EXESS_SUCCESS,
+ EXESS_MAX_DATETIME_LENGTH,
+ -32768,
+ 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 1,
+ true);
+
+ // Simple timezones
+
+ check_read("2001-02-03T04:05:06-00:30",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 4,
+ 35,
+ 6,
+ 0,
+ true);
+
+ check_read("2001-02-03T04:05:06-01:00",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 5,
+ 5,
+ 6,
+ 0,
+ true);
+
+ check_read("2001-02-03T04:05:06+00:30",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 3,
+ 35,
+ 6,
+ 0,
+ true);
+
+ check_read("2001-02-03T04:05:06+01:00",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 3,
+ 5,
+ 6,
+ 0,
+ true);
+
+ // Positive timezone carry
+
+ // Minute => hour
+ check_read("2001-02-03T04:46:00-00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 5,
+ 1,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day
+ check_read("2001-02-03T23:46:00-00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 4,
+ 0,
+ 1,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day => month
+ check_read("2001-02-28T23:46:00-00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 3,
+ 1,
+ 0,
+ 1,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day => month => year
+ check_read("2001-12-31T23:46:00-00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2002,
+ 1,
+ 1,
+ 0,
+ 1,
+ 0,
+ 0,
+ true);
+
+ // Negative timezone carry
+
+ // Minute => hour
+ check_read("2001-02-03T04:14:00+00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 3,
+ 59,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day
+ check_read("2001-02-03T00:14:00+00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 2,
+ 23,
+ 59,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day => month
+ check_read("2001-02-01T00:14:00+00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 1,
+ 31,
+ 23,
+ 59,
+ 0,
+ 0,
+ true);
+
+ // Garbage
+
+ check_read(
+ "2004-04-12T13:00", EXESS_EXPECTED_COLON, 16, 0, 0, 0, 0, 0, 0, 0, false);
+
+ check_read("2004-04-1213:20:00",
+ EXESS_EXPECTED_TIME_SEP,
+ 10,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ false);
+
+ check_read(
+ "99-04-12T13:00", EXESS_EXPECTED_DIGIT, 2, 0, 0, 0, 0, 0, 0, 0, false);
+
+ check_read(
+ "2004-04-12", EXESS_EXPECTED_TIME_SEP, 10, 0, 0, 0, 0, 0, 0, 0, false);
+
+ check_read("2004-04-12-05:00",
+ EXESS_EXPECTED_TIME_SEP,
+ 10,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ false);
+}
+
+static void
+check_write(const ExessDateTime value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DATETIME_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_datetime(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_datetime(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_datetime(void)
+{
+ check_write(local, EXESS_SUCCESS, 20, "2001-02-03T04:05:06");
+ check_write(utc, EXESS_SUCCESS, 21, "2001-02-03T04:05:06Z");
+ check_write(lowest, EXESS_SUCCESS, 22, "-32768-01-01T00:00:00");
+ check_write(highest, EXESS_SUCCESS, 21, "32767-12-31T24:00:00");
+ check_write(nano, EXESS_SUCCESS, 30, "2001-01-01T00:00:00.000000001");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage5, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage6, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage7, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage8, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage9, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage10, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage11, EXESS_BAD_VALUE, 20, "");
+
+ check_write(lowest, EXESS_NO_SPACE, 12, "");
+ check_write(lowest, EXESS_NO_SPACE, 17, "");
+ check_write(lowest, EXESS_NO_SPACE, 18, "");
+ check_write(lowest, EXESS_NO_SPACE, 21, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_datetime(highest, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+int
+main(void)
+{
+ test_add();
+ test_read_datetime();
+ test_write_datetime();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_decimal.c b/subprojects/exess/test/test_decimal.c
new file mode 100644
index 00000000..2a49565d
--- /dev/null
+++ b/subprojects/exess/test/test_decimal.c
@@ -0,0 +1,260 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+#include "int_test_data.h"
+#include "num_test_utils.h"
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const double expected_value,
+ const size_t expected_count)
+{
+ double value = 0;
+ const ExessResult r = exess_read_decimal(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(double_matches(value, expected_value));
+}
+
+static void
+test_read_decimal(void)
+{
+ // No value
+ check_read("", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, (double)NAN, 6);
+
+ // Basic values
+ check_read("1.2", EXESS_SUCCESS, 1.2, 3);
+ check_read("0.01", EXESS_SUCCESS, 0.01, 4);
+ check_read("10.0", EXESS_SUCCESS, 10.0, 4);
+
+ // Non-canonical form
+ check_read(" \f\n\r\t\v42.24 ", EXESS_SUCCESS, 42.24, 11);
+ check_read("12.", EXESS_SUCCESS, 12., 3);
+ check_read(".34", EXESS_SUCCESS, 0.34, 3);
+ check_read("+.56", EXESS_SUCCESS, 0.56, 4);
+ check_read("-.78", EXESS_SUCCESS, -0.78, 4);
+
+ // Limits
+ check_read(
+ "0."
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000022250738585072014",
+ EXESS_SUCCESS,
+ DBL_MIN,
+ 326);
+
+ check_read("1797693134862315700000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000.0",
+ EXESS_SUCCESS,
+ DBL_MAX,
+ 311);
+
+ // Superfluous digits
+ check_read("12345678901234567890", EXESS_SUCCESS, 12345678901234568000.0, 20);
+ check_read("1.2345678901234567890", EXESS_SUCCESS, 1.2345678901234568, 21);
+
+ // Special values
+ check_read("-0.0E0", EXESS_EXPECTED_END, -0.0, 4);
+ check_read("0.0E0", EXESS_EXPECTED_END, 0.0, 3);
+ check_read("+0.0E0", EXESS_EXPECTED_END, 0.0, 4);
+
+ // No exponent
+ check_read("1", EXESS_SUCCESS, 1.0, 1);
+ check_read("2.3", EXESS_SUCCESS, 2.3, 3);
+ check_read("-4.5", EXESS_SUCCESS, -4.5, 4);
+
+ // Garbage
+ check_read("NaN", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read("INF", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read("-INF", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+ check_read("true", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read("+true", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+ check_read("-false", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+}
+
+static void
+test_decimal_string_length(void)
+{
+ // Basic values
+ assert(exess_write_decimal(-1.0, 0, NULL).count == 4);
+ assert(exess_write_decimal(-0.0, 0, NULL).count == 4);
+ assert(exess_write_decimal(0.0, 0, NULL).count == 3);
+ assert(exess_write_decimal(1.0, 0, NULL).count == 3);
+
+ // Limits
+ assert(exess_write_decimal(DBL_MIN, 0, NULL).count == 326);
+ assert(exess_write_decimal(DBL_MAX, 0, NULL).count == 311);
+
+ // Special values
+ assert(exess_write_decimal((double)NAN, 0, NULL).count == 0);
+ assert(exess_write_decimal(-0.0, 0, NULL).count == 4);
+ assert(exess_write_decimal(0.0, 0, NULL).count == 3);
+ assert(exess_write_decimal((double)INFINITY, 0, NULL).count == 0);
+ assert(exess_write_decimal((double)-INFINITY, 0, NULL).count == 0);
+}
+
+/// Check that `str` is a canonical xsd:double string
+static void
+check_canonical(const char* const str)
+{
+ assert(strlen(str) > 2); // Shortest possible is something like 1.2
+ assert(str[0] == '-' || is_digit(str[0]));
+
+ const int first_digit = str[0] == '-' ? 1 : 0;
+
+ for (const char* s = str + first_digit; *s; ++s) {
+ assert(*s == '.' || is_digit(*s));
+ }
+}
+
+static void
+check_write(const double value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DECIMAL_LENGTH + 1] = {42};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_decimal(value, buf_size, buf);
+ assert(r.status == expected_status);
+ if (expected_string) {
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_decimal(value, 0, NULL).count == r.count);
+
+ if (expected_string[0]) {
+ check_canonical(buf);
+ }
+ }
+}
+
+static void
+test_write_decimal(void)
+{
+ check_write((double)NAN, EXESS_BAD_VALUE, 4, "");
+ check_write((double)-INFINITY, EXESS_BAD_VALUE, 5, "");
+
+ check_write(
+ DBL_MIN,
+ EXESS_SUCCESS,
+ 327,
+ "0."
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000022250738585072014");
+
+ check_write(-1.2, EXESS_SUCCESS, 5, "-1.2");
+ check_write(-0.0, EXESS_SUCCESS, 5, "-0.0");
+ check_write(0.0, EXESS_SUCCESS, 4, "0.0");
+ check_write(1.2, EXESS_SUCCESS, 4, "1.2");
+
+ check_write(DBL_MAX,
+ EXESS_SUCCESS,
+ 312,
+ "1797693134862315700000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000.0");
+
+ check_write((double)INFINITY, EXESS_BAD_VALUE, 4, "");
+
+ check_write(DBL_MIN, EXESS_NO_SPACE, 326, "");
+ check_write(-1.2, EXESS_NO_SPACE, 4, "");
+ check_write(-0.0, EXESS_NO_SPACE, 4, "");
+ check_write(0.0, EXESS_NO_SPACE, 3, "");
+ check_write(1.2, EXESS_NO_SPACE, 3, "");
+ check_write(DBL_MAX, EXESS_NO_SPACE, 311, "");
+
+ check_write(-1.0, EXESS_NO_SPACE, 1, "");
+ check_write(-1.0, EXESS_NO_SPACE, 0, NULL);
+}
+
+static void
+check_round_trip(const double value)
+{
+ double parsed_value = 0.0;
+ char buf[EXESS_MAX_DECIMAL_LENGTH + 1] = {42};
+
+ /* fprintf(stderr, "%f\n", value); */
+ assert(!exess_write_decimal(value, sizeof(buf), buf).status);
+ /* fprintf(stderr, "Buf: %s\n", buf); */
+ assert(!exess_read_decimal(&parsed_value, buf).status);
+ assert(double_matches(parsed_value, value));
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ check_round_trip(DBL_MIN);
+ check_round_trip(-0.0);
+ check_round_trip(0.0);
+ check_round_trip(DBL_MAX);
+
+ fprintf(stderr, "Testing xsd:double randomly with seed %u\n", opts.seed);
+
+ uint32_t rep = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ rep = lcg32(rep);
+
+ const double value = double_from_rep(rep);
+
+ check_round_trip(value);
+ print_num_test_progress(i, opts.n_tests);
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_decimal();
+ test_decimal_string_length();
+ test_write_decimal();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_double.c b/subprojects/exess/test/test_double.c
new file mode 100644
index 00000000..d4901c5a
--- /dev/null
+++ b/subprojects/exess/test/test_double.c
@@ -0,0 +1,246 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+#include "int_test_data.h"
+#include "num_test_utils.h"
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const double expected_value,
+ const size_t expected_count)
+{
+ double value = (double)NAN;
+ const ExessResult r = exess_read_double(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(double_matches(value, expected_value));
+}
+
+static void
+test_read_double(void)
+{
+ // Limits
+ check_read("-1.7976931348623157E308", EXESS_SUCCESS, -DBL_MAX, 23);
+ check_read("-2.2250738585072014E-308", EXESS_SUCCESS, -DBL_MIN, 24);
+ check_read("2.2250738585072014E-308", EXESS_SUCCESS, DBL_MIN, 23);
+ check_read("1.7976931348623157E308", EXESS_SUCCESS, DBL_MAX, 22);
+
+ // Beyond limits
+ check_read("1e-326", EXESS_SUCCESS, 0.0, 6);
+ check_read("12345678901234567123", EXESS_SUCCESS, 12345678901234567000.0, 20);
+ check_read("1e309", EXESS_SUCCESS, (double)INFINITY, 5);
+
+ // Non-canonical form
+ check_read("+1E3", EXESS_SUCCESS, 1e3, 4);
+ check_read("1E+3", EXESS_SUCCESS, 1e3, 4);
+ check_read("+1.5E3", EXESS_SUCCESS, 1.5e3, 6);
+ check_read(".5E3", EXESS_SUCCESS, 0.5e3, 4);
+ check_read("+.5E3", EXESS_SUCCESS, 0.5e3, 5);
+ check_read("-.5E3", EXESS_SUCCESS, -0.5e3, 5);
+ check_read("1.E3", EXESS_SUCCESS, 1e3, 4);
+ check_read("+1.E3", EXESS_SUCCESS, 1e3, 5);
+ check_read("-1.E3", EXESS_SUCCESS, -1e3, 5);
+
+ // Special values
+ check_read("NaN", EXESS_SUCCESS, (double)NAN, 3);
+ check_read("-INF", EXESS_SUCCESS, (double)-INFINITY, 4);
+ check_read("-0.0E0", EXESS_SUCCESS, -0.0, 6);
+ check_read("0.0E0", EXESS_SUCCESS, 0.0, 5);
+ check_read("+0.0E0", EXESS_SUCCESS, 0.0, 6);
+ check_read("INF", EXESS_SUCCESS, (double)INFINITY, 3);
+ check_read("+INF", EXESS_SUCCESS, (double)INFINITY, 4);
+
+ // No exponent
+ check_read("1", EXESS_SUCCESS, 1.0, 1);
+ check_read("2.3", EXESS_SUCCESS, 2.3, 3);
+ check_read("-4.5", EXESS_SUCCESS, -4.5, 4);
+
+ // Leading whitespace
+ check_read(" \f\n\r\t\v1.2", EXESS_SUCCESS, 1.2, 9);
+
+ // Garbage
+ check_read("true", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read("+true", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+ check_read("-false", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+ check_read("1.0eX", EXESS_EXPECTED_DIGIT, (double)NAN, 4);
+ check_read("1.0EX", EXESS_EXPECTED_DIGIT, (double)NAN, 4);
+}
+
+/// Check that `str` is a canonical xsd:double string
+static void
+check_canonical(const char* const str)
+{
+ if (!strcmp(str, "NaN") || !strcmp(str, "-INF") || !strcmp(str, "INF")) {
+ return;
+ }
+
+ assert(strlen(str) > 4); // Shortest possible is something like 1.2E3
+ assert(str[0] == '-' || is_digit(str[0]));
+
+ const int first_digit = str[0] == '-' ? 1 : 0;
+ assert(is_digit(str[first_digit]));
+ assert(str[first_digit + 1] == '.');
+ assert(is_digit(str[first_digit + 2]));
+
+ const char* const e = strchr(str, 'E');
+ assert(e);
+ assert(*e == 'E');
+ assert(*(e + 1) == '-' || is_digit(*(e + 1)));
+}
+
+static void
+check_write(const double value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DOUBLE_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, //
+ 21, 22, 23};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_double(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert((r.status && r.status != EXESS_NO_SPACE) ||
+ exess_write_double(value, 0, NULL).count == r.count);
+
+ if (!r.status) {
+ check_canonical(buf);
+ }
+}
+
+static void
+test_write_double(void)
+{
+ check_write((double)NAN, EXESS_SUCCESS, 4, "NaN");
+ check_write(DBL_MIN, EXESS_SUCCESS, 24, "2.2250738585072014E-308");
+ check_write(-0.0, EXESS_SUCCESS, 7, "-0.0E0");
+ check_write(0.0, EXESS_SUCCESS, 6, "0.0E0");
+ check_write(DBL_MAX, EXESS_SUCCESS, 23, "1.7976931348623157E308");
+
+ /* check_write((double)NAN, EXESS_NO_SPACE, 3, ""); */
+ /* check_write(DBL_MIN, EXESS_SUCCESS, 24, "2.2250738585072014E-308"); */
+ /* check_write(-0.0, EXESS_SUCCESS, 7, "-0.0E0"); */
+ /* check_write(0.0, EXESS_SUCCESS, 6, "0.0E0"); */
+ /* check_write(DBL_MAX, EXESS_SUCCESS, 23, "1.7976931348623157E308"); */
+}
+
+static void
+check_round_trip(const double value)
+{
+ double parsed_value = 0.0;
+
+ char buf[EXESS_MAX_DOUBLE_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, //
+ 21, 22, 23};
+
+ assert(!exess_write_double(value, sizeof(buf), buf).status);
+ assert(!exess_read_double(&parsed_value, buf).status);
+ assert(double_matches(parsed_value, value));
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ check_round_trip((double)NAN);
+ check_round_trip(-(double)INFINITY);
+ check_round_trip(DBL_MIN);
+ check_round_trip(-0.0);
+ check_round_trip(0.0);
+ check_round_trip(DBL_MAX);
+ check_round_trip((double)INFINITY);
+
+ check_round_trip(5.0);
+ check_round_trip(50.0);
+ check_round_trip(500000000000000000000.0);
+ check_round_trip(-0.5);
+ check_round_trip(0.5);
+ check_round_trip(0.05);
+ check_round_trip(0.005);
+ check_round_trip(0.00000000000000000005);
+
+ // Normal limits
+ check_round_trip(nextafter(DBL_MIN, (double)INFINITY));
+ check_round_trip(nextafter(DBL_EPSILON, (double)INFINITY));
+ check_round_trip(nextafter(DBL_MAX, -(double)INFINITY));
+
+ // Subnormals
+ check_round_trip(nextafter(0.0, 1.0));
+ check_round_trip(nextafter(nextafter(0.0, 1.0), 2.0));
+ check_round_trip(nextafter(0.0, -1.0));
+ check_round_trip(nextafter(nextafter(0.0, -1.0), -2.0));
+
+ // Various tricky cases
+ check_round_trip(1e23);
+ check_round_trip(6.02951420360127e-309);
+ check_round_trip(9.17857104364115e+288);
+ check_round_trip(2.68248422823759e+22);
+
+ // Powers of two (where the lower boundary is closer)
+ for (int i = -1023; i <= 1023; ++i) {
+ check_round_trip(pow(2, i));
+ }
+
+ fprintf(stderr, "Testing xsd:double randomly with seed %u\n", opts.seed);
+
+ uint64_t rep = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ rep = lcg64(rep);
+
+ const double value = double_from_rep(rep);
+
+ check_round_trip(nextafter(value, -(double)INFINITY));
+ check_round_trip(value);
+ check_round_trip(nextafter(value, (double)INFINITY));
+
+ print_num_test_progress(i, opts.n_tests);
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_double();
+ test_write_double();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_duration.c b/subprojects/exess/test/test_duration.c
new file mode 100644
index 00000000..3d964e80
--- /dev/null
+++ b/subprojects/exess/test/test_duration.c
@@ -0,0 +1,310 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "int_test_data.h"
+#include "macros.h"
+#include "num_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static const ExessDuration zero = {0, 0, 0};
+static const ExessDuration lowest = {-INT32_MAX, -INT32_MAX, -999999999};
+static const ExessDuration highest = {INT32_MAX, INT32_MAX, 999999999};
+static const ExessDuration year = {12, 0, 0};
+static const ExessDuration month = {1, 0, 0};
+static const ExessDuration day = {0, 24 * 60 * 60, 0};
+static const ExessDuration hour = {0, 60 * 60, 0};
+static const ExessDuration minute = {0, 60, 0};
+static const ExessDuration second = {0, 1, 0};
+static const ExessDuration nanosecond = {0, 0, 1};
+
+static const ExessDuration n_year = {-12, 0, 0};
+static const ExessDuration n_month = {-1, 0, 0};
+static const ExessDuration n_day = {0, -24 * 60 * 60, 0};
+static const ExessDuration n_hour = {0, -60 * 60, 0};
+static const ExessDuration n_minute = {0, -60, 0};
+static const ExessDuration n_second = {0, -1, 0};
+static const ExessDuration n_nanosecond = {0, 0, -1};
+
+static const ExessDuration garbage1 = {1, 1, -1};
+static const ExessDuration garbage2 = {1, -1, 1};
+static const ExessDuration garbage3 = {1, -1, -1};
+static const ExessDuration garbage4 = {-1, 1, 1};
+static const ExessDuration garbage5 = {-1, 1, -1};
+static const ExessDuration garbage6 = {-1, -1, 1};
+static const ExessDuration garbage7 = {INT32_MIN, 0, -999999999};
+static const ExessDuration garbage8 = {0, INT32_MIN, -999999999};
+static const ExessDuration garbage9 = {INT32_MIN, INT32_MIN, -999999999};
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_count,
+ const int32_t expected_years,
+ const int32_t expected_months,
+ const int32_t expected_days,
+ const int32_t expected_hours,
+ const int32_t expected_minutes,
+ const int32_t expected_seconds,
+ const int32_t expected_nanoseconds,
+ const bool expected_is_negative)
+{
+ ExessDuration value = {0, 0, 0.0f};
+ const ExessResult r = exess_read_duration(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+
+ assert(value.months == (expected_is_negative ? -1 : 1) * 12 * expected_years +
+ expected_months);
+
+ assert(value.seconds ==
+ (expected_is_negative ? -1 : 1) *
+ ((expected_seconds + (60 * expected_minutes) +
+ (60 * 60 * expected_hours) + (24 * 60 * 60 * expected_days))));
+
+ assert(value.nanoseconds == expected_nanoseconds);
+}
+
+static void
+test_read_duration(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_DURATION, 0, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ " \f\n\r\t\v", EXESS_EXPECTED_DURATION, 6, 0, 0, 0, 0, 0, 0, 0, false);
+
+ // Good values
+
+ check_read(
+ "P2Y6M5DT12H35M30S", EXESS_SUCCESS, 17, 2, 6, 5, 12, 35, 30, 0, false);
+
+ check_read("P1DT2H", EXESS_SUCCESS, 6, 0, 0, 1, 2, 0, 0, 0, false);
+ check_read("P20M", EXESS_SUCCESS, 4, 0, 20, 0, 0, 0, 0, 0, false);
+ check_read("PT20M", EXESS_SUCCESS, 5, 0, 0, 0, 0, 20, 0, 0, false);
+ check_read("P0Y20M0D", EXESS_SUCCESS, 8, 0, 20, 0, 0, 0, 0, 0, false);
+ check_read("P0Y", EXESS_SUCCESS, 3, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("-P60D", EXESS_SUCCESS, 5, 0, 0, 60, 0, 0, 0, 0, true);
+ check_read(
+ "PT1M30.5S", EXESS_SUCCESS, 9, 0, 0, 0, 0, 1, 30, 500000000, false);
+
+ // Leading and trailing whitespace
+ check_read(" \f\n\r\t\vP1Y", EXESS_SUCCESS, 9, 1, 0, 0, 0, 0, 0, 0, false);
+ check_read("P1MT2H \f\n\r\t\v", EXESS_SUCCESS, 6, 0, 1, 0, 2, 0, 0, 0, false);
+ check_read(" \f\n\r\t\vP1Y", EXESS_SUCCESS, 9, 1, 0, 0, 0, 0, 0, 0, false);
+ check_read("P1YT2H \f\n\r\t\v", EXESS_SUCCESS, 6, 1, 0, 0, 2, 0, 0, 0, false);
+
+ // Non-canonical form
+ check_read("P06D", EXESS_SUCCESS, 4, 0, 0, 6, 0, 0, 0, 0, false);
+ check_read("PT7.0S", EXESS_SUCCESS, 6, 0, 0, 0, 0, 0, 7, 0, false);
+ check_read(
+ "P0Y0M01DT06H00M00S", EXESS_SUCCESS, 18, 0, 0, 1, 6, 0, 0, 0, false);
+
+ // Out of range fields
+ check_read(
+ "P2147483647Y", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P2147483647M", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P2147483647D", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT2147483647H", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT2147483647M", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT2147483647S", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+
+ // Garbage
+ check_read("P-20M", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P20MT", EXESS_EXPECTED_DIGIT, 5, 0, 20, 0, 0, 0, 0, 0, false);
+ check_read("P1YM5D", EXESS_EXPECTED_DIGIT, 3, 1, 0, 0, 0, 0, 0, 0, false);
+ check_read("P15.5Y", EXESS_EXPECTED_DATE_TAG, 3, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P1D2H", EXESS_EXPECTED_TIME_SEP, 3, 0, 0, 1, 0, 0, 0, 0, false);
+ check_read("1Y2M", EXESS_EXPECTED_DURATION, 0, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P2M1Y", EXESS_BAD_ORDER, 4, 0, 2, 0, 0, 0, 0, 0, false);
+ check_read("P2D1Y", EXESS_EXPECTED_TIME_SEP, 3, 0, 0, 2, 0, 0, 0, 0, false);
+ check_read("P2D1M", EXESS_EXPECTED_TIME_SEP, 3, 0, 0, 2, 0, 0, 0, 0, false);
+ check_read("P", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT15.5H", EXESS_EXPECTED_TIME_TAG, 6, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT2M1H", EXESS_BAD_ORDER, 5, 0, 0, 0, 0, 2, 0, 0, false);
+ check_read("PT2S1H", EXESS_EXPECTED_END, 4, 0, 0, 0, 0, 0, 2, 0, false);
+ check_read("PT2S1M", EXESS_EXPECTED_END, 4, 0, 0, 0, 0, 0, 2, 0, false);
+ check_read("PT15.S", EXESS_EXPECTED_DIGIT, 5, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P1Q", EXESS_EXPECTED_DATE_TAG, 2, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT1Q", EXESS_EXPECTED_TIME_TAG, 3, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P-1Y", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P-1M", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P-1D", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT-1H", EXESS_EXPECTED_DIGIT, 2, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT-1M", EXESS_EXPECTED_DIGIT, 2, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT-1S", EXESS_EXPECTED_DIGIT, 2, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P4294967296Y", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P4294967296M", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P4294967296D", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT4294967296H", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT4294967296M", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("", EXESS_EXPECTED_DURATION, 0, 0, 0, 0, 0, 0, 0, 0, false);
+}
+
+static void
+check_write(const ExessDuration value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DURATION_LENGTH + 1] = {42};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_duration(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_duration(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_duration(void)
+{
+ check_write(zero, EXESS_SUCCESS, 4, "P0Y");
+
+ check_write(
+ lowest, EXESS_SUCCESS, 39, "-P178956970Y7M24855DT3H14M7.999999999S");
+
+ check_write(
+ highest, EXESS_SUCCESS, 38, "P178956970Y7M24855DT3H14M7.999999999S");
+
+ check_write(year, EXESS_SUCCESS, 4, "P1Y");
+ check_write(month, EXESS_SUCCESS, 4, "P1M");
+ check_write(day, EXESS_SUCCESS, 4, "P1D");
+ check_write(hour, EXESS_SUCCESS, 5, "PT1H");
+ check_write(minute, EXESS_SUCCESS, 5, "PT1M");
+ check_write(second, EXESS_SUCCESS, 5, "PT1S");
+ check_write(nanosecond, EXESS_SUCCESS, 15, "PT0.000000001S");
+
+ check_write(n_year, EXESS_SUCCESS, 5, "-P1Y");
+ check_write(n_month, EXESS_SUCCESS, 5, "-P1M");
+ check_write(n_day, EXESS_SUCCESS, 5, "-P1D");
+ check_write(n_hour, EXESS_SUCCESS, 6, "-PT1H");
+ check_write(n_minute, EXESS_SUCCESS, 6, "-PT1M");
+ check_write(n_second, EXESS_SUCCESS, 6, "-PT1S");
+ check_write(n_nanosecond, EXESS_SUCCESS, 16, "-PT0.000000001S");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage5, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage6, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage7, EXESS_OUT_OF_RANGE, 41, "");
+ check_write(garbage8, EXESS_OUT_OF_RANGE, 41, "");
+ check_write(garbage9, EXESS_OUT_OF_RANGE, 41, "");
+
+ check_write(zero, EXESS_NO_SPACE, 3, "");
+ check_write(lowest, EXESS_NO_SPACE, 24, "");
+ check_write(highest, EXESS_NO_SPACE, 4, "");
+ check_write(highest, EXESS_NO_SPACE, 10, "");
+ check_write(highest, EXESS_NO_SPACE, 13, "");
+ check_write(highest, EXESS_NO_SPACE, 16, "");
+ check_write(highest, EXESS_NO_SPACE, 20, "");
+ check_write(highest, EXESS_NO_SPACE, 23, "");
+ check_write(year, EXESS_NO_SPACE, 3, "");
+ check_write(month, EXESS_NO_SPACE, 3, "");
+ check_write(day, EXESS_NO_SPACE, 3, "");
+ check_write(hour, EXESS_NO_SPACE, 4, "");
+ check_write(minute, EXESS_NO_SPACE, 4, "");
+ check_write(second, EXESS_NO_SPACE, 4, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_duration(zero, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+static void
+check_round_trip(const ExessDuration value)
+{
+ ExessDuration parsed_value = {0, 0, 0};
+ char buf[EXESS_MAX_DURATION_LENGTH + 1] = {0};
+
+ assert(exess_write_duration(value, 0, NULL).count <=
+ EXESS_MAX_DURATION_LENGTH);
+
+ assert(!exess_write_duration(value, sizeof(buf), buf).status);
+ assert(!exess_read_duration(&parsed_value, buf).status);
+ assert(parsed_value.months == value.months);
+ assert(parsed_value.seconds == value.seconds);
+ assert(parsed_value.nanoseconds == value.nanoseconds);
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ fprintf(stderr, "Testing xsd:duration randomly with seed %u\n", opts.seed);
+
+ const uint64_t n_tests = MAX(256, opts.n_tests / 16);
+
+ uint32_t rng = opts.seed;
+ for (size_t i = 0; i < n_tests; ++i) {
+ rng = lcg32(rng);
+
+ const int32_t months = (int32_t)rng;
+
+ rng = lcg32(rng);
+
+ const int32_t seconds = (months < 0 ? -1 : 1) * (int32_t)(rng % INT32_MAX);
+
+ rng = lcg32(rng);
+
+ const int32_t nanoseconds =
+ (months < 0 ? -1 : 1) * (int32_t)(rng % 1000000000);
+
+ const ExessDuration value = {months, seconds, nanoseconds};
+ check_round_trip(value);
+
+ print_num_test_progress(i, n_tests);
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_duration();
+ test_write_duration();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_float.c b/subprojects/exess/test/test_float.c
new file mode 100644
index 00000000..e8cf9141
--- /dev/null
+++ b/subprojects/exess/test/test_float.c
@@ -0,0 +1,240 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+#include "int_test_data.h"
+#include "num_test_utils.h"
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const float expected_value,
+ const size_t expected_count)
+{
+ float value = NAN;
+ const ExessResult r = exess_read_float(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(float_matches(value, expected_value));
+}
+
+static void
+test_read_float(void)
+{
+ // Limits
+ check_read("-3.40282347E38", EXESS_SUCCESS, -FLT_MAX, 14);
+ check_read("-1.17549435E-38", EXESS_SUCCESS, -FLT_MIN, 15);
+ check_read("1.17549435E-38", EXESS_SUCCESS, FLT_MIN, 14);
+ check_read("3.40282347E38", EXESS_SUCCESS, FLT_MAX, 13);
+
+ // Special values
+ check_read("NaN", EXESS_SUCCESS, NAN, 3);
+ check_read("-INF", EXESS_SUCCESS, -INFINITY, 4);
+ check_read("-0.0E0", EXESS_SUCCESS, -0.0f, 6);
+ check_read("0.0E0", EXESS_SUCCESS, 0.0f, 5);
+ check_read("+0.0E0", EXESS_SUCCESS, 0.0f, 6);
+ check_read("INF", EXESS_SUCCESS, INFINITY, 3);
+ check_read("+INF", EXESS_SUCCESS, INFINITY, 4);
+
+ // Various normal cases
+ check_read("-1.0E0", EXESS_SUCCESS, -1.0f, 6);
+ check_read("1.0E0", EXESS_SUCCESS, +1.0f, 5);
+ check_read("5.0E0", EXESS_SUCCESS, 5.0f, 5);
+ check_read("5.0E1", EXESS_SUCCESS, 50.0f, 5);
+ check_read("5.0E9", EXESS_SUCCESS, 5000000000.0f, 5);
+ check_read("-5.0E-1", EXESS_SUCCESS, -0.5f, 7);
+ check_read("5.0E-1", EXESS_SUCCESS, 0.5f, 6);
+ check_read("6.25E-2", EXESS_SUCCESS, 0.0625f, 7);
+ check_read("7.8125E-3", EXESS_SUCCESS, 0.0078125f, 9);
+
+ // No exponent
+ check_read("1", EXESS_SUCCESS, 1.0f, 1);
+ check_read("2.3", EXESS_SUCCESS, 2.3f, 3);
+ check_read("-4.5", EXESS_SUCCESS, -4.5f, 4);
+
+ // Trailing garbage
+ check_read("1.2.", EXESS_SUCCESS, 1.2f, 3);
+
+ // Garbage
+ check_read("true", EXESS_EXPECTED_DIGIT, NAN, 0);
+ check_read("+true", EXESS_EXPECTED_DIGIT, NAN, 1);
+ check_read("-false", EXESS_EXPECTED_DIGIT, NAN, 1);
+ check_read("1.0eX", EXESS_EXPECTED_DIGIT, NAN, 4);
+ check_read("1.0EX", EXESS_EXPECTED_DIGIT, NAN, 4);
+}
+
+static void
+test_float_string_length(void)
+{
+ // Limits
+ assert(exess_write_float(FLT_MIN, 0, NULL).count == 14);
+ assert(exess_write_float(FLT_MAX, 0, NULL).count == 13);
+
+ // Special values
+ assert(exess_write_float((float)NAN, 0, NULL).count == 3);
+ assert(exess_write_float(-1.0f, 0, NULL).count == 6);
+ assert(exess_write_float(-0.0f, 0, NULL).count == 6);
+ assert(exess_write_float(0.0f, 0, NULL).count == 5);
+ assert(exess_write_float(1.0f, 0, NULL).count == 5);
+ assert(exess_write_float((float)INFINITY, 0, NULL).count == 3);
+ assert(exess_write_float((float)-INFINITY, 0, NULL).count == 4);
+}
+
+/// Check that `str` is a canonical xsd:float string
+static void
+check_canonical(const char* const str)
+{
+ if (!strcmp(str, "NaN") || !strcmp(str, "-INF") || !strcmp(str, "INF")) {
+ return;
+ }
+
+ assert(strlen(str) > 4); // Shortest possible is something like 1.2E3
+ assert(str[0] == '-' || is_digit(str[0]));
+
+ const int first_digit = str[0] == '-' ? 1 : 0;
+ assert(is_digit(str[first_digit]));
+ assert(str[first_digit + 1] == '.');
+ assert(is_digit(str[first_digit + 2]));
+
+ const char* const e = strchr(str, 'E');
+ assert(e);
+ assert(*e == 'E');
+ assert(*(e + 1) == '-' || is_digit(*(e + 1)));
+}
+
+static void
+check_write(const float value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_FLOAT_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_float(value, buf_size, buf);
+ assert(r.status == expected_status);
+ if (expected_string) {
+ assert(r.count == strlen(buf));
+ assert(!expected_string || !strcmp(buf, expected_string));
+ assert(r.status || exess_write_float(value, 0, NULL).count == r.count);
+ check_canonical(buf);
+ } else {
+ assert(r.count == 0);
+ }
+}
+
+static void
+test_write_float(void)
+{
+ check_write(NAN, EXESS_SUCCESS, 4, "NaN");
+ check_write(-INFINITY, EXESS_SUCCESS, 5, "-INF");
+ check_write(FLT_MIN, EXESS_SUCCESS, 15, "1.17549435E-38");
+ check_write(-0.0f, EXESS_SUCCESS, 7, "-0.0E0");
+ check_write(0.0f, EXESS_SUCCESS, 6, "0.0E0");
+ check_write(100.25f, EXESS_SUCCESS, 9, "1.0025E2");
+ check_write(FLT_MAX, EXESS_SUCCESS, 14, "3.40282346E38");
+ check_write(INFINITY, EXESS_SUCCESS, 4, "INF");
+
+ check_write(NAN, EXESS_NO_SPACE, 3, NULL);
+ check_write(-INFINITY, EXESS_NO_SPACE, 4, NULL);
+ check_write(FLT_MIN, EXESS_NO_SPACE, 13, NULL);
+ check_write(-1.0f, EXESS_NO_SPACE, 2, NULL);
+ check_write(-0.0f, EXESS_NO_SPACE, 6, NULL);
+ check_write(0.0f, EXESS_NO_SPACE, 5, NULL);
+ check_write(100.25f, EXESS_NO_SPACE, 5, NULL);
+ check_write(100.25f, EXESS_NO_SPACE, 8, NULL);
+ check_write(FLT_MAX, EXESS_NO_SPACE, 13, NULL);
+ check_write(INFINITY, EXESS_NO_SPACE, 3, NULL);
+}
+
+static void
+check_round_trip(const float value)
+{
+ float parsed_value = 0.0f;
+ char buf[EXESS_MAX_FLOAT_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+
+ assert(!exess_write_float(value, sizeof(buf), buf).status);
+ assert(!exess_read_float(&parsed_value, buf).status);
+ assert(float_matches(parsed_value, value));
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ check_round_trip(NAN);
+ check_round_trip(FLT_MIN);
+ check_round_trip(-0.0f);
+ check_round_trip(0.0f);
+ check_round_trip(FLT_MAX);
+
+ if (opts.exhaustive) {
+ fprintf(stderr, "Testing xsd:float exhaustively\n");
+
+ for (int64_t i = 0; i <= UINT32_MAX; ++i) {
+ const float value = float_from_rep((uint32_t)i);
+
+ check_round_trip(value);
+ print_num_test_progress((uint64_t)(i - (int64_t)INT32_MIN), UINT32_MAX);
+ }
+ } else {
+ fprintf(stderr, "Testing xsd:float randomly with seed %u\n", opts.seed);
+
+ uint32_t rep = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ rep = lcg32(rep);
+
+ const float value = float_from_rep(rep);
+
+ check_round_trip(nextafterf(value, -INFINITY));
+ check_round_trip(value);
+ check_round_trip(nextafterf(value, INFINITY));
+
+ print_num_test_progress(i, opts.n_tests);
+ }
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_float();
+ test_float_string_length();
+ test_write_float();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_hex.c b/subprojects/exess/test/test_hex.c
new file mode 100644
index 00000000..5ceec90b
--- /dev/null
+++ b/subprojects/exess/test/test_hex.c
@@ -0,0 +1,180 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_value_length,
+ const char* const expected_value,
+ const size_t expected_value_size,
+ const size_t expected_count)
+{
+ char buf[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
+ ExessBlob blob = {sizeof(buf), buf};
+
+ ExessResult r = exess_read_hex(&blob, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(r.status || blob.size == expected_value_size);
+ if (expected_value_length > 0) {
+ assert(!strncmp(buf, expected_value, expected_value_length));
+ assert(blob.size <= exess_hex_decoded_size(strlen(string)));
+ }
+}
+
+static void
+test_lowercase(void)
+{
+ char buf[6] = {0, 0, 0, 0, 0, 0};
+ ExessBlob blob = {sizeof(buf), buf};
+
+ ExessResult r = exess_read_hex(&blob, "6A6B6C6D6E6F");
+ assert(r.status == EXESS_SUCCESS);
+ assert(r.count == 12);
+ assert(blob.size == 6);
+ assert(!strncmp((const char*)blob.data, "jklmno", 6));
+
+ r = exess_read_hex(&blob, "6a6b6c6d6e6f");
+ assert(r.status == EXESS_SUCCESS);
+ assert(r.count == 12);
+ assert(blob.size == 6);
+ assert(!strncmp((const char*)blob.data, "jklmno", 6));
+}
+
+static void
+test_whitespace(void)
+{
+ check_read("666F6F", EXESS_SUCCESS, 3, "foo", 3, 6);
+ check_read(" 666F6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read("6\f66F6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read("66\n6F6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read("666\rF6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read("666F\t6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read(" \f\n\r\t\v666F6F", EXESS_SUCCESS, 3, "foo", 3, 12);
+ check_read("666F6F \f\n\r\t\v", EXESS_SUCCESS, 3, "foo", 3, 12);
+}
+
+static void
+test_syntax_errors(void)
+{
+ check_read("G6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("g6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("!6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("^6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("6G", EXESS_EXPECTED_HEX, 0, NULL, 0, 2);
+ check_read("6g", EXESS_EXPECTED_HEX, 0, NULL, 0, 2);
+ check_read("6!", EXESS_EXPECTED_HEX, 0, NULL, 0, 2);
+ check_read("6^", EXESS_EXPECTED_HEX, 0, NULL, 0, 2);
+ check_read("6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("66G6", EXESS_EXPECTED_HEX, 0, NULL, 1, 3);
+ check_read("66 G6", EXESS_EXPECTED_HEX, 0, NULL, 1, 4);
+}
+
+static void
+test_read_overflow(void)
+{
+ char buf[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
+ ExessBlob blob0 = {0, buf};
+ ExessBlob blob1 = {1, buf};
+ ExessBlob blob2 = {2, buf};
+
+ ExessResult r = exess_read_hex(&blob0, "666F6F");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 2);
+ assert(blob0.size == 0);
+
+ r = exess_read_hex(&blob1, "666F6F");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 4);
+ assert(blob1.size == 1);
+
+ r = exess_read_hex(&blob2, "666F6F");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 6);
+ assert(blob2.size == 2);
+}
+
+static void
+test_write_overflow(void)
+{
+ char buf[7] = {1, 2, 3, 4, 5, 6, 7};
+
+ assert(exess_write_hex(3, "foo", 0, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 1, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 2, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 3, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 4, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 5, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 6, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 7, buf).status == EXESS_SUCCESS);
+}
+
+static void
+test_round_trip(void)
+{
+ for (size_t size = 1; size < 256; ++size) {
+ // Allocate and generate data
+ uint8_t* const data = (uint8_t*)malloc(size);
+ for (size_t i = 0; i < size; ++i) {
+ data[i] = (uint8_t)((size + i) % 256);
+ }
+
+ // Allocate buffer for encoding with minimum required size
+ const size_t str_len = exess_write_hex(size, data, 0, NULL).count;
+ char* const str = (char*)malloc(str_len + 1);
+
+ // Encode data to string buffer
+ assert(!exess_write_hex(size, data, str_len + 1, str).status);
+ assert(strlen(str) == str_len);
+ assert(str_len % 2 == 0);
+
+ // Allocate buffer for decoded data with the same size as the input
+ uint8_t* const decoded = (uint8_t*)malloc(size);
+ ExessBlob decoded_blob = {size, decoded};
+
+ // Decode and check that data matches the original input
+ assert(!exess_read_hex(&decoded_blob, str).status);
+ assert(decoded_blob.size == size);
+ assert(!memcmp(decoded, data, size));
+
+ free(decoded);
+ free(str);
+ free(data);
+ }
+}
+
+int
+main(void)
+{
+ test_lowercase();
+ test_whitespace();
+ test_syntax_errors();
+ test_read_overflow();
+ test_write_overflow();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_int.c b/subprojects/exess/test/test_int.c
new file mode 100644
index 00000000..d3e43f7c
--- /dev/null
+++ b/subprojects/exess/test/test_int.c
@@ -0,0 +1,129 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "int_test_data.h"
+#include "num_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int32_t expected_value,
+ const size_t expected_count)
+{
+ int32_t value = 0;
+ const ExessResult r = exess_read_int(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_int(void)
+{
+ // Limits
+ check_read("-2147483648", EXESS_SUCCESS, INT32_MIN, EXESS_MAX_INT_LENGTH);
+ check_read("2147483647", EXESS_SUCCESS, INT32_MAX, 10);
+
+ // Out of range
+ check_read("-2147483649", EXESS_OUT_OF_RANGE, 0, 11);
+ check_read("2147483648", EXESS_OUT_OF_RANGE, 0, 10);
+ check_read("10000000000", EXESS_OUT_OF_RANGE, 0, 11);
+
+ // Garbage
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 1);
+}
+
+static void
+check_write(const int32_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_INT_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_int(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_int(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_int(void)
+{
+ check_write(INT32_MIN, EXESS_SUCCESS, 12, "-2147483648");
+ check_write(INT32_MAX, EXESS_SUCCESS, 11, "2147483647");
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ int32_t parsed_value = 0;
+ char buf[EXESS_MAX_INT_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+
+ if (opts.exhaustive) {
+ fprintf(stderr, "Testing xsd:int exhaustively\n");
+
+ for (int64_t i = INT32_MIN; i <= INT32_MAX; ++i) {
+ assert(!exess_write_int((int32_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_int(&parsed_value, buf).status);
+ assert(parsed_value == i);
+
+ print_num_test_progress((uint64_t)(i - (int64_t)INT32_MIN), UINT32_MAX);
+ }
+ } else {
+ fprintf(stderr, "Testing xsd:int randomly with seed %u\n", opts.seed);
+ uint32_t rep = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ rep = lcg32(rep);
+
+ const int32_t value = (int32_t)rep;
+
+ assert(!exess_write_int(value, sizeof(buf), buf).status);
+ assert(!exess_read_int(&parsed_value, buf).status);
+ assert(parsed_value == value);
+
+ print_num_test_progress(i, opts.n_tests);
+ }
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_int();
+ test_write_int();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_int_math.c b/subprojects/exess/test/test_int_math.c
new file mode 100644
index 00000000..8ab3f63f
--- /dev/null
+++ b/subprojects/exess/test/test_int_math.c
@@ -0,0 +1,97 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "attributes.h"
+#include "int_math.h"
+
+#include <assert.h>
+#include <stdint.h>
+
+static void
+test_clz32(void)
+{
+ for (unsigned i = 0; i < 32; ++i) {
+ assert(exess_clz32(1u << i) == 32u - i - 1u);
+ }
+}
+
+static void
+test_clz64(void)
+{
+ for (unsigned i = 0; i < 64; ++i) {
+ assert(exess_clz64(1ull << i) == 64u - i - 1u);
+ }
+}
+
+static void
+test_ilog2(void)
+{
+ for (unsigned i = 0; i < 64; ++i) {
+ assert(exess_ilog2(1ull << i) == i);
+ }
+}
+
+static void
+test_ilog10(void)
+{
+ uint64_t power = 1;
+ for (unsigned i = 0; i < 20; ++i, power *= 10) {
+ assert(exess_ilog10(power) == i);
+ }
+}
+
+static void
+test_num_digits(void)
+{
+ assert(1 == exess_num_digits(0));
+ assert(1 == exess_num_digits(1));
+ assert(1 == exess_num_digits(9));
+ assert(2 == exess_num_digits(10));
+ assert(2 == exess_num_digits(99ull));
+ assert(3 == exess_num_digits(999ull));
+ assert(4 == exess_num_digits(9999ull));
+ assert(5 == exess_num_digits(99999ull));
+ assert(6 == exess_num_digits(999999ull));
+ assert(7 == exess_num_digits(9999999ull));
+ assert(8 == exess_num_digits(99999999ull));
+ assert(9 == exess_num_digits(999999999ull));
+ assert(10 == exess_num_digits(9999999999ull));
+ assert(11 == exess_num_digits(99999999999ull));
+ assert(12 == exess_num_digits(999999999999ull));
+ assert(13 == exess_num_digits(9999999999999ull));
+ assert(14 == exess_num_digits(99999999999999ull));
+ assert(15 == exess_num_digits(999999999999999ull));
+ assert(16 == exess_num_digits(9999999999999999ull));
+ assert(17 == exess_num_digits(99999999999999999ull));
+ assert(18 == exess_num_digits(999999999999999999ull));
+ assert(19 == exess_num_digits(9999999999999999999ull));
+ assert(20 == exess_num_digits(18446744073709551615ull));
+}
+
+EXESS_I_PURE_FUNC
+int
+main(void)
+{
+ test_clz32();
+ test_clz64();
+ test_ilog2();
+ test_ilog10();
+ test_num_digits();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_long.c b/subprojects/exess/test/test_long.c
new file mode 100644
index 00000000..5631ca9f
--- /dev/null
+++ b/subprojects/exess/test/test_long.c
@@ -0,0 +1,144 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int64_t expected_value,
+ const size_t expected_count)
+{
+ int64_t value = 0;
+
+ const ExessResult r = exess_read_long(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_long(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 0, 6);
+
+ // Canonical form
+ check_read("-1", EXESS_SUCCESS, -1, 2);
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("1", EXESS_SUCCESS, 1, 1);
+ check_read("1234", EXESS_SUCCESS, 1234, 4);
+ check_read("-1234", EXESS_SUCCESS, -1234, 5);
+
+ // Non-canonical form
+ check_read(" \f\n\r\t\v1234 ", EXESS_SUCCESS, 1234, 10);
+ check_read(" \f\n\r\t\v-1234 ", EXESS_SUCCESS, -1234, 11);
+ check_read(" \f\n\r\t\v+1234 ", EXESS_SUCCESS, 1234, 11);
+ check_read(" \f\n\r\t\v01234 ", EXESS_SUCCESS, 1234, 11);
+ check_read(" \f\n\r\t\v-01234 ", EXESS_SUCCESS, -1234, 12);
+ check_read("-01", EXESS_SUCCESS, -1, 3);
+ check_read("-0", EXESS_SUCCESS, 0, 2);
+ check_read("-00", EXESS_SUCCESS, 0, 3);
+ check_read("00", EXESS_SUCCESS, 0, 2);
+ check_read("+0", EXESS_SUCCESS, 0, 2);
+ check_read("+00", EXESS_SUCCESS, 0, 3);
+ check_read("+1", EXESS_SUCCESS, 1, 2);
+ check_read("+01", EXESS_SUCCESS, 1, 3);
+ check_read("+1234", EXESS_SUCCESS, 1234, 5);
+ check_read("01234", EXESS_SUCCESS, 1234, 5);
+ check_read("-01234", EXESS_SUCCESS, -1234, 6);
+
+ // Limits
+ check_read(
+ "-9223372036854775808", EXESS_SUCCESS, INT64_MIN, EXESS_MAX_LONG_LENGTH);
+ check_read("9223372036854775807", EXESS_SUCCESS, INT64_MAX, 19);
+
+ // Out of range
+ check_read("-9223372036854775809", EXESS_OUT_OF_RANGE, 0, 20);
+ check_read("9223372036854775808", EXESS_OUT_OF_RANGE, 0, 19);
+ check_read("12345678901234567890", EXESS_OUT_OF_RANGE, 0, 20);
+
+ // Trailing garbage
+ check_read("1234extra", EXESS_EXPECTED_END, 1234, 4);
+
+ // Garbage
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 1);
+ check_read("-", EXESS_EXPECTED_DIGIT, 0, 1);
+ check_read("true", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("false", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("zero", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("NaN", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("INF", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("-INF", EXESS_EXPECTED_DIGIT, 0, 1);
+}
+
+static void
+check_write(const int64_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_LONG_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, //
+ 21};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_long(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_long(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_long(void)
+{
+ check_write(-1, EXESS_SUCCESS, 3, "-1");
+ check_write(0, EXESS_SUCCESS, 2, "0");
+ check_write(1, EXESS_SUCCESS, 2, "1");
+ check_write(INT64_MIN, EXESS_SUCCESS, 21, "-9223372036854775808");
+ check_write(INT64_MAX, EXESS_SUCCESS, 20, "9223372036854775807");
+
+ check_write(INT64_MIN, EXESS_NO_SPACE, 20, "");
+ check_write(INT64_MAX, EXESS_NO_SPACE, 19, "");
+ check_write(1234, EXESS_NO_SPACE, 4, "");
+ check_write(-1234, EXESS_NO_SPACE, 5, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_long(1234, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+int
+main(void)
+{
+ test_read_long();
+ test_write_long();
+ return 0;
+}
diff --git a/subprojects/exess/test/test_short.c b/subprojects/exess/test/test_short.c
new file mode 100644
index 00000000..e0f3aa82
--- /dev/null
+++ b/subprojects/exess/test/test_short.c
@@ -0,0 +1,100 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int16_t expected_value,
+ const size_t expected_count)
+{
+ int16_t value = 0;
+
+ const ExessResult r = exess_read_short(&value, string);
+ assert(value == expected_value);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+}
+
+static void
+test_read_short(void)
+{
+ // Limits
+ check_read("-32768", EXESS_SUCCESS, INT16_MIN, EXESS_MAX_SHORT_LENGTH);
+ check_read("32767", EXESS_SUCCESS, INT16_MAX, 5);
+
+ // Out of range
+ check_read("-32769", EXESS_OUT_OF_RANGE, 0, 6);
+ check_read("32768", EXESS_OUT_OF_RANGE, 0, 5);
+
+ // Garbage
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 1);
+}
+
+static void
+check_write(const int16_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_SHORT_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_short(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_short(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_short(void)
+{
+ check_write(INT16_MIN, EXESS_SUCCESS, 7, "-32768");
+ check_write(INT16_MAX, EXESS_SUCCESS, 6, "32767");
+}
+
+static void
+test_round_trip(void)
+{
+ int16_t value = 0;
+ char buf[EXESS_MAX_SHORT_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ for (int32_t i = INT16_MIN; i <= INT16_MAX; ++i) {
+ assert(!exess_write_short((int16_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_short(&value, buf).status);
+ assert(value == i);
+ }
+}
+
+int
+main(void)
+{
+ test_read_short();
+ test_write_short();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_strerror.c b/subprojects/exess/test/test_strerror.c
new file mode 100644
index 00000000..1e5534ad
--- /dev/null
+++ b/subprojects/exess/test/test_strerror.c
@@ -0,0 +1,35 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <string.h>
+
+int
+main(void)
+{
+ for (ExessStatus i = EXESS_SUCCESS; i <= EXESS_UNSUPPORTED;
+ i = (ExessStatus)(i + 1)) {
+ assert(strlen(exess_strerror(i)) > 0);
+ }
+
+ assert(!strcmp(exess_strerror((ExessStatus)9999), "Unknown error"));
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_time.c b/subprojects/exess/test/test_time.c
new file mode 100644
index 00000000..6cf20f6d
--- /dev/null
+++ b/subprojects/exess/test/test_time.c
@@ -0,0 +1,222 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+#include "int_test_data.h"
+#include "time_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+static const ExessTime nozone = {{EXESS_LOCAL}, 0, 0, 0, 0};
+static const ExessTime utc = {{0}, 12, 15, 1, 250000000};
+static const ExessTime zoned = {INIT_ZONE(11, 30), 23, 59, 59, 1000000};
+static const ExessTime high = {INIT_ZONE(11, 30), 24, 0, 0, 0};
+static const ExessTime garbage1 = {INIT_ZONE(11, 30), 0, 0, 0, 1000000000};
+static const ExessTime garbage2 = {INIT_ZONE(11, 30), 0, 0, 60, 0};
+static const ExessTime garbage3 = {INIT_ZONE(11, 30), 0, 60, 0, 0};
+static const ExessTime garbage4 = {INIT_ZONE(11, 30), 24, 0, 0, 1};
+static const ExessTime garbage5 = {INIT_ZONE(11, 30), 24, 0, 1, 0};
+static const ExessTime garbage6 = {INIT_ZONE(11, 30), 24, 1, 0, 0};
+static const ExessTime garbage7 = {INIT_ZONE(11, 30), 25, 0, 0, 0};
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint8_t expected_hour,
+ const uint8_t expected_minute,
+ const uint8_t expected_second,
+ const uint32_t expected_nanosecond,
+ const int8_t expected_tz_hour,
+ const int8_t expected_tz_minute,
+ const bool expected_tz_is_present,
+ const size_t expected_count)
+{
+ ExessTime value = {{0}, 0, 0, 0, 0};
+
+ const ExessResult r = exess_read_time(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value.hour == expected_hour);
+ assert(value.minute == expected_minute);
+ assert(value.second == expected_second);
+ assert(value.nanosecond == expected_nanosecond);
+ assert((!expected_tz_is_present && value.zone.quarter_hours == EXESS_LOCAL) ||
+ value.zone.quarter_hours ==
+ 4 * expected_tz_hour + expected_tz_minute / 15);
+}
+
+static void
+test_read_time(void)
+{
+ // No value
+ check_read("", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, 0, false, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, 0, false, 6);
+
+ // Good values
+ check_read("13:20:00", EXESS_SUCCESS, 13, 20, 0, 0, 0, 0, false, 8);
+ check_read(
+ "13:20:30.5555", EXESS_SUCCESS, 13, 20, 30, 555500000, 0, 0, false, 13);
+ check_read("13:20:00-05:00", EXESS_SUCCESS, 13, 20, 0, 0, -5, 0, true, 14);
+ check_read("13:20:00Z", EXESS_SUCCESS, 13, 20, 0, 0, 0, 0, true, 9);
+ check_read("00:00:00", EXESS_SUCCESS, 0, 0, 0, 0, 0, 0, false, 8);
+ check_read("24:00:00", EXESS_SUCCESS, 24, 0, 0, 0, 0, 0, false, 8);
+ check_read("21:32:52", EXESS_SUCCESS, 21, 32, 52, 0, 0, 0, false, 8);
+ check_read("21:32:52+02:00", EXESS_SUCCESS, 21, 32, 52, 0, 2, 0, true, 14);
+ check_read("19:32:52Z", EXESS_SUCCESS, 19, 32, 52, 0, 0, 0, true, 9);
+ check_read("19:32:52+00:00", EXESS_SUCCESS, 19, 32, 52, 0, 0, 0, true, 14);
+ check_read(
+ "21:32:52.12679", EXESS_SUCCESS, 21, 32, 52, 126790000, 0, 0, false, 14);
+
+ // Longest possible string
+ check_read("24:59:59.000000001-14:00",
+ EXESS_SUCCESS,
+ 24,
+ 59,
+ 59,
+ 1,
+ -14,
+ 0,
+ true,
+ EXESS_MAX_TIME_LENGTH);
+
+ // Non-canonical form
+ check_read(
+ " \f\n\r\t\v13:20:00 ", EXESS_SUCCESS, 13, 20, 0, 0, 0, 0, false, 14);
+
+ // Trailing garbage
+ check_read("13:20:00junk", EXESS_EXPECTED_SIGN, 13, 20, 0, 0, 0, 0, false, 8);
+ check_read("13:20:00Zjunk", EXESS_EXPECTED_END, 13, 20, 0, 0, 0, 0, true, 9);
+
+ // Garbage
+ check_read("13.20.00", EXESS_EXPECTED_COLON, 13, 0, 0, 0, 0, 0, false, 2);
+ check_read("13:20:", EXESS_EXPECTED_DIGIT, 13, 20, 0, 0, 0, 0, false, 6);
+ check_read("5:20:00", EXESS_EXPECTED_DIGIT, 5, 0, 0, 0, 0, 0, false, 1);
+ check_read("13:20", EXESS_EXPECTED_COLON, 13, 20, 0, 0, 0, 0, false, 5);
+ check_read("13:20.5:00", EXESS_EXPECTED_COLON, 13, 20, 0, 0, 0, 0, false, 5);
+ check_read("13:65:00", EXESS_OUT_OF_RANGE, 13, 65, 0, 0, 0, 0, false, 5);
+ check_read("21:32", EXESS_EXPECTED_COLON, 21, 32, 0, 0, 0, 0, false, 5);
+ check_read("25:25:10", EXESS_OUT_OF_RANGE, 25, 0, 0, 0, 0, 0, false, 2);
+ check_read("-10:00:00", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, 0, false, 0);
+ check_read("1:20:10", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, false, 1);
+ check_read("13:20:00A", EXESS_EXPECTED_SIGN, 13, 20, 0, 0, 0, 0, false, 8);
+}
+
+static void
+check_write(const ExessTime value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_TIME_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9,
+ 10, 11, 12, 13, 14, 15, 16, 17, 18,
+ 19, 20, 21, 22, 23, 24, 25};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_time(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_time(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_time(void)
+{
+ check_write(nozone, EXESS_SUCCESS, 9, "00:00:00");
+ check_write(utc, EXESS_SUCCESS, 13, "12:15:01.25Z");
+ check_write(zoned, EXESS_SUCCESS, 19, "23:59:59.001+11:30");
+ check_write(high, EXESS_SUCCESS, 15, "24:00:00+11:30");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage5, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage6, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage7, EXESS_BAD_VALUE, 19, "");
+
+ check_write(nozone, EXESS_NO_SPACE, 8, "");
+ check_write(utc, EXESS_NO_SPACE, 12, "");
+ check_write(zoned, EXESS_NO_SPACE, 18, "");
+ check_write(zoned, EXESS_NO_SPACE, 12, "");
+ check_write(high, EXESS_NO_SPACE, 14, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_time(nozone, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+static void
+check_round_trip(const ExessTime value)
+{
+ ExessTime parsed_value = {{0}, 0, 0, 0, 0};
+ char buf[EXESS_MAX_TIME_LENGTH + 1] = {0};
+
+ assert(!exess_write_time(value, sizeof(buf), buf).status);
+ assert(!exess_read_time(&parsed_value, buf).status);
+ assert(parsed_value.hour == value.hour);
+ assert(parsed_value.minute == value.minute);
+ assert(double_matches(parsed_value.second, value.second));
+ assert(parsed_value.zone.quarter_hours == value.zone.quarter_hours);
+}
+
+static void
+test_round_trip(void)
+{
+ uint32_t rng = 0;
+ for (uint8_t h = 0; h < 24; ++h) {
+ for (uint8_t m = 0; m < 60; ++m) {
+ rng = lcg32(rng);
+
+ const uint32_t ns = rng % 1000000000u;
+
+ rng = lcg32(rng);
+
+ const uint8_t s = (uint8_t)(rng % 60u);
+ const ExessTime no_zone = {{EXESS_LOCAL}, h, m, s, ns};
+ const ExessTime lowest_zone = {INIT_ZONE(-14, 0), h, m, s, ns};
+ const ExessTime highest_zone = {INIT_ZONE(14, 0), h, m, s, ns};
+
+ check_round_trip(no_zone);
+ check_round_trip(lowest_zone);
+ check_round_trip(highest_zone);
+
+ const ExessTime value = {random_timezone(&rng), h, m, s, ns};
+ check_round_trip(value);
+ }
+ }
+}
+
+int
+main(void)
+{
+ test_read_time();
+ test_write_time();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_timezone.c b/subprojects/exess/test/test_timezone.c
new file mode 100644
index 00000000..8ccfb12b
--- /dev/null
+++ b/subprojects/exess/test/test_timezone.c
@@ -0,0 +1,178 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "time_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <stdio.h>
+
+static const ExessTimezone missing = {EXESS_LOCAL};
+static const ExessTimezone utc = INIT_ZONE(0, 0);
+static const ExessTimezone plus = INIT_ZONE(11, 30);
+static const ExessTimezone minus = INIT_ZONE(-11, -30);
+static const ExessTimezone slight = INIT_ZONE(0, 30);
+static const ExessTimezone lowest = INIT_ZONE(-14, 0);
+static const ExessTimezone highest = INIT_ZONE(14, 0);
+static const ExessTimezone garbage1 = INIT_ZONE(-14, -15);
+static const ExessTimezone garbage2 = INIT_ZONE(14, 15);
+static const ExessTimezone garbage3 = INIT_ZONE(-15, 0);
+static const ExessTimezone garbage4 = INIT_ZONE(15, 0);
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int8_t expected_hour,
+ const int8_t expected_minute,
+ const bool expected_is_present,
+ const size_t expected_count)
+{
+ // The timezone interface is not public, so we test it via time
+ char time_string[] = "12:00:00XXXXXX";
+ strncpy(time_string + 8, string, sizeof(time_string) - 9);
+
+ ExessTime value = {{0}, 0, 0, 0, 0};
+ const ExessResult r = exess_read_time(&value, time_string);
+
+ assert(r.status == expected_status);
+ assert(r.count == 8 + expected_count);
+ assert((!expected_is_present && value.zone.quarter_hours == EXESS_LOCAL) ||
+ value.zone.quarter_hours == 4 * expected_hour + expected_minute / 15);
+}
+
+static void
+test_read_timezone(void)
+{
+ // Basic values
+ check_read("Z", EXESS_SUCCESS, 0, 0, true, 1);
+ check_read("-05:00", EXESS_SUCCESS, -5, 0, true, 6);
+ check_read("+02:00", EXESS_SUCCESS, 2, 0, true, 6);
+ check_read("+00:00", EXESS_SUCCESS, 0, 0, true, 6);
+ check_read("-00:00", EXESS_SUCCESS, 0, 0, true, 6);
+
+ // Limits
+ check_read("-14:00", EXESS_SUCCESS, -14, 0, true, 6);
+ check_read("+14:00", EXESS_SUCCESS, 14, 0, true, 6);
+ check_read("-13:45", EXESS_SUCCESS, -13, -45, true, 6);
+ check_read("+13:45", EXESS_SUCCESS, 13, 45, true, 6);
+
+ // Out of range
+ check_read("-14:15", EXESS_OUT_OF_RANGE, 0, 0, false, 6);
+ check_read("+14:15", EXESS_OUT_OF_RANGE, 0, 0, false, 6);
+ check_read("-15:00", EXESS_OUT_OF_RANGE, 0, 0, false, 3);
+ check_read("+15:00", EXESS_OUT_OF_RANGE, 0, 0, false, 3);
+ check_read("-13:60", EXESS_OUT_OF_RANGE, 0, 0, false, 6);
+ check_read("+13:60", EXESS_OUT_OF_RANGE, 0, 0, false, 6);
+
+ // Garbage
+ check_read("+05:01", EXESS_UNSUPPORTED, 0, 0, false, 6);
+ check_read("05:00", EXESS_EXPECTED_SIGN, 0, 0, false, 0);
+ check_read("+5:00", EXESS_EXPECTED_DIGIT, 0, 0, false, 2);
+ check_read("+5:0", EXESS_EXPECTED_DIGIT, 0, 0, false, 2);
+ check_read("+5:", EXESS_EXPECTED_DIGIT, 0, 0, false, 2);
+ check_read("+:0", EXESS_EXPECTED_DIGIT, 0, 0, false, 1);
+ check_read("+A5:00", EXESS_EXPECTED_DIGIT, 0, 0, false, 1);
+ check_read("+0A:00", EXESS_EXPECTED_DIGIT, 0, 0, false, 2);
+ check_read("+05A00", EXESS_EXPECTED_COLON, 0, 0, false, 3);
+ check_read("+05:A0", EXESS_EXPECTED_DIGIT, 0, 0, false, 4);
+ check_read("+05:0A", EXESS_EXPECTED_DIGIT, 0, 0, false, 5);
+}
+
+static void
+check_write(const ExessTimezone value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ // The timezone interface is not public, so we test it via time
+ char buf[EXESS_MAX_TIME_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9,
+ 10, 11, 12, 13, 14, 15, 16, 17, 18,
+ 19, 20, 21, 22, 23, 24, 25};
+
+ assert(buf_size <= sizeof(buf) - 8);
+
+ const ExessTime time = {value, 12, 0, 0, 0};
+ const ExessResult r = exess_write_time(time, 8 + buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert((!buf[0] && !expected_string[0]) || !strcmp(buf + 8, expected_string));
+ assert(r.status || exess_write_time(time, 0, NULL).count == r.count);
+}
+
+static void
+test_write_timezone(void)
+{
+ check_write(missing, EXESS_SUCCESS, 1, "");
+ check_write(utc, EXESS_SUCCESS, 2, "Z");
+ check_write(plus, EXESS_SUCCESS, 7, "+11:30");
+ check_write(minus, EXESS_SUCCESS, 7, "-11:30");
+ check_write(slight, EXESS_SUCCESS, 7, "+00:30");
+ check_write(lowest, EXESS_SUCCESS, 7, "-14:00");
+ check_write(highest, EXESS_SUCCESS, 7, "+14:00");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 7, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 7, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 7, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 7, "");
+
+ check_write(utc, EXESS_NO_SPACE, 1, "");
+ check_write(plus, EXESS_NO_SPACE, 6, "");
+}
+
+static void
+check_round_trip(const ExessTimezone value)
+{
+ ExessTime parsed_time = {{0}, 0, 0, 0, 0};
+ char buf[EXESS_MAX_TIME_LENGTH + 1] = {42};
+
+ const ExessTime time = {value, 12, 0, 0, 0};
+
+ assert(!exess_write_time(time, sizeof(buf), buf).status);
+ assert(!exess_read_time(&parsed_time, buf).status);
+ assert(!memcmp(&parsed_time.zone, &value, sizeof(ExessTimezone)));
+}
+
+static void
+test_round_trip(void)
+{
+ check_round_trip(lowest);
+ check_round_trip(highest);
+
+ for (int8_t h = -13; h < 13; ++h) {
+ for (int8_t q = 0; q < 4; ++q) {
+ const ExessTimezone value = {(int8_t)(4 * h + q)};
+
+ check_round_trip(value);
+ }
+ }
+}
+
+int
+main(void)
+{
+ test_read_timezone();
+ test_write_timezone();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_ubyte.c b/subprojects/exess/test/test_ubyte.c
new file mode 100644
index 00000000..7129c85d
--- /dev/null
+++ b/subprojects/exess/test/test_ubyte.c
@@ -0,0 +1,100 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint8_t expected_value,
+ const size_t expected_count)
+{
+ uint8_t value = 0;
+
+ const ExessResult r = exess_read_ubyte(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_ubyte(void)
+{
+ // Limits
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("255", EXESS_SUCCESS, UINT8_MAX, EXESS_MAX_UBYTE_LENGTH);
+
+ // Out of range
+ check_read("256", EXESS_OUT_OF_RANGE, 0, 3);
+
+ // Garbage
+ check_read("-1", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 0);
+}
+
+static void
+check_write(const uint8_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_UBYTE_LENGTH + 1] = {1, 2, 3, 4};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_ubyte(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_ubyte(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_ubyte(void)
+{
+ check_write(0, EXESS_SUCCESS, 2, "0");
+ check_write(UINT8_MAX, EXESS_SUCCESS, 4, "255");
+}
+
+static void
+test_round_trip(void)
+{
+ uint8_t value = 0;
+ char buf[EXESS_MAX_UBYTE_LENGTH + 1] = {1, 2, 3, 4};
+
+ for (uint16_t i = 0; i <= UINT8_MAX; ++i) {
+ assert(!exess_write_ubyte((uint8_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_ubyte(&value, buf).status);
+ assert(value == i);
+ }
+}
+
+int
+main(void)
+{
+ test_read_ubyte();
+ test_write_ubyte();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_uint.c b/subprojects/exess/test/test_uint.c
new file mode 100644
index 00000000..5889fc20
--- /dev/null
+++ b/subprojects/exess/test/test_uint.c
@@ -0,0 +1,128 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "int_test_data.h"
+#include "num_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint32_t expected_value,
+ const size_t expected_count)
+{
+ uint32_t value = 0;
+ const ExessResult r = exess_read_uint(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_uint(void)
+{
+ // Limits
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("4294967295", EXESS_SUCCESS, UINT32_MAX, EXESS_MAX_UINT_LENGTH);
+
+ // Out of range
+ check_read("4294967296", EXESS_OUT_OF_RANGE, 0, 10);
+
+ // Garbage
+ check_read("-1", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 0);
+}
+
+static void
+check_write(const uint32_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_UINT_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_uint(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_uint(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_uint(void)
+{
+ check_write(0u, EXESS_SUCCESS, 2, "0");
+ check_write(UINT32_MAX, EXESS_SUCCESS, 11, "4294967295");
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ uint32_t parsed_value = 0;
+ char buf[EXESS_MAX_UINT_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+ if (opts.exhaustive) {
+ fprintf(stderr, "Testing xsd:unsignedInt exhaustively\n");
+
+ for (uint64_t i = 0u; i <= UINT32_MAX; ++i) {
+ assert(!exess_write_uint((uint32_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_uint(&parsed_value, buf).status);
+ assert(parsed_value == i);
+
+ print_num_test_progress(i, UINT32_MAX);
+ }
+ } else {
+ fprintf(
+ stderr, "Testing xsd:unsignedInt randomly with seed %u\n", opts.seed);
+
+ uint32_t value = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ value = lcg32(value);
+
+ assert(!exess_write_uint(value, sizeof(buf), buf).status);
+ assert(!exess_read_uint(&parsed_value, buf).status);
+ assert(parsed_value == value);
+
+ print_num_test_progress(i, opts.n_tests);
+ }
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_uint();
+ test_write_uint();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_ulong.c b/subprojects/exess/test/test_ulong.c
new file mode 100644
index 00000000..ba8ba05f
--- /dev/null
+++ b/subprojects/exess/test/test_ulong.c
@@ -0,0 +1,126 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint64_t expected_value,
+ const size_t expected_count)
+{
+ uint64_t value = 0;
+ const ExessResult r = exess_read_ulong(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_ulong(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 0, 6);
+
+ // Canonical form
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("1234", EXESS_SUCCESS, 1234, 4);
+
+ // Non-canonical form
+ check_read(" \f\n\r\t\v1234 ", EXESS_SUCCESS, 1234, 10);
+ check_read(" \f\n\r\t\v01234 ", EXESS_SUCCESS, 1234, 11);
+ check_read("01234", EXESS_SUCCESS, 1234, 5);
+ check_read("00", EXESS_SUCCESS, 0, 2);
+
+ // Limits
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read(
+ "18446744073709551615", EXESS_SUCCESS, UINT64_MAX, EXESS_MAX_ULONG_LENGTH);
+
+ // Out of range
+ check_read("18446744073709551616", EXESS_OUT_OF_RANGE, 0, 19);
+
+ // Trailing garbage
+ check_read("1234extra", EXESS_EXPECTED_END, 1234, 4);
+
+ // Garbage
+ check_read(" \f\n\r\t\v+1234 ", EXESS_EXPECTED_DIGIT, 0, 6);
+ check_read("+1234", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+0", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("-", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("true", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("false", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("zero", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("NaN", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("INF", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("-INF", EXESS_EXPECTED_DIGIT, 0, 0);
+}
+
+static void
+check_write(const uint64_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_ULONG_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, //
+ 21};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_ulong(value, buf_size, buf);
+
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_ulong(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_ulong(void)
+{
+ check_write(0u, EXESS_SUCCESS, 2, "0");
+ check_write(1u, EXESS_SUCCESS, 2, "1");
+ check_write(UINT64_MAX, EXESS_SUCCESS, 21, "18446744073709551615");
+
+ check_write(1234u, EXESS_NO_SPACE, 4, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_ulong(1234u, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+int
+main(void)
+{
+ test_read_ulong();
+ test_write_ulong();
+ return 0;
+}
diff --git a/subprojects/exess/test/test_ushort.c b/subprojects/exess/test/test_ushort.c
new file mode 100644
index 00000000..056e182a
--- /dev/null
+++ b/subprojects/exess/test/test_ushort.c
@@ -0,0 +1,100 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint16_t expected_value,
+ const size_t expected_count)
+{
+ uint16_t value = 0;
+
+ const ExessResult r = exess_read_ushort(&value, string);
+ assert(value == expected_value);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+}
+
+static void
+test_read_ushort(void)
+{
+ // Limits
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("65535", EXESS_SUCCESS, UINT16_MAX, EXESS_MAX_USHORT_LENGTH);
+
+ // Out of range
+ check_read("65536", EXESS_OUT_OF_RANGE, 0, 5);
+
+ // Garbage
+ check_read("-1", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 0);
+}
+
+static void
+check_write(const uint16_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_USHORT_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_ushort(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_ushort(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_ushort(void)
+{
+ check_write(0u, EXESS_SUCCESS, 2, "0");
+ check_write(UINT16_MAX, EXESS_SUCCESS, 6, "65535");
+}
+
+static void
+test_round_trip(void)
+{
+ uint16_t value = 0;
+ char buf[EXESS_MAX_USHORT_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ for (uint32_t i = 0; i <= UINT16_MAX; ++i) {
+ assert(!exess_write_ushort((uint16_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_ushort(&value, buf).status);
+ assert(value == i);
+ }
+}
+
+int
+main(void)
+{
+ test_read_ushort();
+ test_write_ushort();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_variant.c b/subprojects/exess/test/test_variant.c
new file mode 100644
index 00000000..7ef7803d
--- /dev/null
+++ b/subprojects/exess/test/test_variant.c
@@ -0,0 +1,301 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#define CHECK_POINTEE_EQUALS(p, v) assert((p) && (*(p) == (v)))
+
+static const ExessDuration duration = {14,
+ 3 * 24 * 60 * 60 + 4 * 60 * 60 + 5 * 60 +
+ 6,
+ 0};
+
+static const ExessDateTime datetime = {2001, 2, 3, false, 4, 5, 6, 0};
+static const ExessTime time = {{0}, 1, 2, 3, 0};
+static const ExessDate date = {2001, 2, 3, {0}};
+
+static void
+check_read(ExessVariant* const variant,
+ const ExessDatatype datatype,
+ const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_count)
+{
+ const ExessResult r = exess_read_variant(variant, datatype, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(variant->datatype == datatype);
+}
+
+static void
+test_read_variant(void)
+{
+ ExessVariant variant;
+
+ check_read(&variant, EXESS_NOTHING, "42", EXESS_UNSUPPORTED, 0);
+
+ check_read(&variant, EXESS_DECIMAL, "1.2", EXESS_SUCCESS, 3);
+ CHECK_POINTEE_EQUALS(exess_get_double(&variant), 1.2);
+
+ check_read(&variant, EXESS_DOUBLE, "3.4", EXESS_SUCCESS, 3);
+ assert(variant.value.as_double == 3.4);
+
+ check_read(&variant, EXESS_FLOAT, "5.6", EXESS_SUCCESS, 3);
+ assert(variant.value.as_float == 5.6f);
+
+ check_read(&variant, EXESS_BOOLEAN, "true", EXESS_SUCCESS, 4);
+ assert(variant.value.as_bool);
+
+ check_read(&variant, EXESS_INTEGER, "7", EXESS_SUCCESS, 1);
+ assert(variant.value.as_long == 7);
+
+ check_read(
+ &variant, EXESS_NON_POSITIVE_INTEGER, "f", EXESS_EXPECTED_DIGIT, 0);
+ check_read(&variant, EXESS_NON_POSITIVE_INTEGER, "1", EXESS_OUT_OF_RANGE, 1);
+ check_read(&variant, EXESS_NON_POSITIVE_INTEGER, "-8", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == -8);
+
+ check_read(&variant, EXESS_NEGATIVE_INTEGER, "f", EXESS_EXPECTED_DIGIT, 0);
+ check_read(&variant, EXESS_NEGATIVE_INTEGER, "1", EXESS_OUT_OF_RANGE, 1);
+ check_read(&variant, EXESS_NEGATIVE_INTEGER, "-9", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == -9);
+
+ check_read(&variant, EXESS_LONG, "10", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == 10);
+
+ check_read(&variant, EXESS_INT, "11", EXESS_SUCCESS, 2);
+ assert(variant.value.as_int == 11);
+
+ check_read(&variant, EXESS_SHORT, "12", EXESS_SUCCESS, 2);
+ assert(variant.value.as_short == 12);
+
+ check_read(&variant, EXESS_BYTE, "13", EXESS_SUCCESS, 2);
+ assert(variant.value.as_byte == 13);
+
+ check_read(&variant, EXESS_ULONG, "14", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == 14);
+
+ check_read(&variant, EXESS_UINT, "15", EXESS_SUCCESS, 2);
+ assert(variant.value.as_int == 15);
+
+ check_read(&variant, EXESS_USHORT, "16", EXESS_SUCCESS, 2);
+ assert(variant.value.as_short == 16);
+
+ check_read(&variant, EXESS_UBYTE, "17", EXESS_SUCCESS, 2);
+ assert(variant.value.as_byte == 17);
+
+ check_read(&variant, EXESS_POSITIVE_INTEGER, "-1", EXESS_EXPECTED_DIGIT, 0);
+ check_read(&variant, EXESS_POSITIVE_INTEGER, "0", EXESS_OUT_OF_RANGE, 1);
+ check_read(&variant, EXESS_POSITIVE_INTEGER, "18", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == 18);
+
+ check_read(&variant, EXESS_DATE, "2001-01-02", EXESS_SUCCESS, 10);
+ assert(variant.value.as_date.year == 2001);
+ assert(variant.value.as_date.month == 1);
+ assert(variant.value.as_date.day == 2);
+
+ check_read(&variant, EXESS_TIME, "12:15:01.25", EXESS_SUCCESS, 11);
+ assert(variant.value.as_time.hour == 12);
+ assert(variant.value.as_time.minute == 15);
+ assert(variant.value.as_time.second == 1);
+ assert(variant.value.as_time.nanosecond == 250000000);
+
+ char blob_data[] = {0, 0, 0};
+
+ variant.datatype = EXESS_HEX;
+ variant.value.as_blob.size = sizeof(blob_data);
+ variant.value.as_blob.data = blob_data;
+ check_read(&variant, EXESS_HEX, "666F6F", EXESS_SUCCESS, 6);
+ assert(!strncmp(blob_data, "foo", sizeof(blob_data)));
+
+ variant.datatype = EXESS_BASE64;
+ variant.value.as_blob.size = sizeof(blob_data);
+ variant.value.as_blob.data = blob_data;
+ check_read(&variant, EXESS_BASE64, "Zm9v", EXESS_SUCCESS, 4);
+ assert(!strncmp(blob_data, "foo", sizeof(blob_data)));
+}
+
+static void
+test_variant_string_length(void)
+{
+ const ExessVariant variant = {EXESS_DECIMAL, {.as_double = 12.3456}};
+
+ assert(exess_write_variant(variant, 0, NULL).count == 7);
+}
+
+static void
+check_write(const ExessVariant value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[328] = {42};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_variant(value, buf_size, buf);
+ assert(r.status == expected_status);
+ if (buf_size > 0) {
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ }
+}
+
+static void
+test_write_variant(void)
+{
+ char blob_data[] = {'f', 'o', 'o'};
+ const ExessBlob blob = {sizeof(blob_data), blob_data};
+
+ const ExessVariant a_nothing = exess_make_nothing(EXESS_SUCCESS);
+ const ExessVariant a_bool = exess_make_boolean(true);
+ const ExessVariant a_decimal = exess_make_decimal(1.2);
+ const ExessVariant a_double = exess_make_double(3.4);
+ const ExessVariant a_float = exess_make_float(5.6f);
+ const ExessVariant a_long = exess_make_long(7);
+ const ExessVariant a_int = exess_make_int(8);
+ const ExessVariant a_short = exess_make_short(9);
+ const ExessVariant a_byte = exess_make_byte(10);
+ const ExessVariant a_ulong = exess_make_ulong(11);
+ const ExessVariant a_uint = exess_make_uint(12);
+ const ExessVariant a_ushort = exess_make_ushort(13);
+ const ExessVariant a_ubyte = exess_make_ubyte(14);
+ const ExessVariant a_duration = exess_make_duration(duration);
+ const ExessVariant a_datetime = exess_make_datetime(datetime);
+ const ExessVariant a_time = exess_make_time(time);
+ const ExessVariant a_date = exess_make_date(date);
+ const ExessVariant a_hex = exess_make_hex(blob);
+ const ExessVariant a_base64 = exess_make_base64(blob);
+
+ check_write(a_nothing, EXESS_BAD_VALUE, 0, "");
+ check_write(a_nothing, EXESS_BAD_VALUE, 1, "");
+ check_write(a_decimal, EXESS_SUCCESS, 4, "1.2");
+ check_write(a_double, EXESS_SUCCESS, 6, "3.4E0");
+ check_write(a_float, EXESS_SUCCESS, 12, "5.5999999E0");
+ check_write(a_bool, EXESS_SUCCESS, 5, "true");
+ check_write(a_long, EXESS_SUCCESS, 2, "7");
+ check_write(a_int, EXESS_SUCCESS, 2, "8");
+ check_write(a_short, EXESS_SUCCESS, 2, "9");
+ check_write(a_byte, EXESS_SUCCESS, 3, "10");
+ check_write(a_ulong, EXESS_SUCCESS, 3, "11");
+ check_write(a_uint, EXESS_SUCCESS, 3, "12");
+ check_write(a_ushort, EXESS_SUCCESS, 3, "13");
+ check_write(a_ubyte, EXESS_SUCCESS, 3, "14");
+ check_write(a_duration, EXESS_SUCCESS, 15, "P1Y2M3DT4H5M6S");
+ check_write(a_datetime, EXESS_SUCCESS, 40, "2001-02-03T04:05:06");
+ check_write(a_time, EXESS_SUCCESS, 40, "01:02:03Z");
+ check_write(a_date, EXESS_SUCCESS, 40, "2001-02-03Z");
+ check_write(a_hex, EXESS_SUCCESS, 7, "666F6F");
+ check_write(a_base64, EXESS_SUCCESS, 5, "Zm9v");
+
+ const ExessBlob null_blob = {0, NULL};
+
+ const ExessVariant null_hex = exess_make_hex(null_blob);
+ check_write(null_hex, EXESS_BAD_VALUE, 99, "");
+
+ const ExessVariant null_base64 = exess_make_base64(null_blob);
+ check_write(null_base64, EXESS_BAD_VALUE, 99, "");
+}
+
+static void
+test_make_get(void)
+{
+ char blob_data[] = {'f', 'o', 'o'};
+ const ExessBlob blob = {sizeof(blob_data), blob_data};
+
+ const ExessVariant a_nothing = exess_make_nothing(EXESS_NO_SPACE);
+ const ExessVariant a_bool = exess_make_boolean(true);
+ const ExessVariant a_decimal = exess_make_decimal(1.2);
+ const ExessVariant a_double = exess_make_double(3.4);
+ const ExessVariant a_float = exess_make_float(5.6f);
+ const ExessVariant a_long = exess_make_long(7);
+ const ExessVariant a_int = exess_make_int(8);
+ const ExessVariant a_short = exess_make_short(9);
+ const ExessVariant a_byte = exess_make_byte(10);
+ const ExessVariant a_ulong = exess_make_ulong(11);
+ const ExessVariant a_uint = exess_make_uint(12);
+ const ExessVariant a_ushort = exess_make_ushort(13);
+ const ExessVariant a_ubyte = exess_make_ubyte(14);
+ const ExessVariant a_duration = exess_make_duration(duration);
+ const ExessVariant a_datetime = exess_make_datetime(datetime);
+ const ExessVariant a_time = exess_make_time(time);
+ const ExessVariant a_date = exess_make_date(date);
+ const ExessVariant a_hex = exess_make_hex(blob);
+ const ExessVariant a_base64 = exess_make_base64(blob);
+
+ // Different types as status
+ assert(exess_get_status(&a_nothing) == EXESS_NO_SPACE);
+ assert(exess_get_status(&a_bool) == EXESS_SUCCESS);
+
+ // Basic successful get
+ CHECK_POINTEE_EQUALS(exess_get_boolean(&a_bool), true);
+ CHECK_POINTEE_EQUALS(exess_get_double(&a_decimal), 1.2);
+ CHECK_POINTEE_EQUALS(exess_get_double(&a_double), 3.4);
+ CHECK_POINTEE_EQUALS(exess_get_float(&a_float), 5.6f);
+ CHECK_POINTEE_EQUALS(exess_get_long(&a_long), 7);
+ CHECK_POINTEE_EQUALS(exess_get_int(&a_int), 8);
+ CHECK_POINTEE_EQUALS(exess_get_short(&a_short), 9);
+ CHECK_POINTEE_EQUALS(exess_get_byte(&a_byte), 10);
+ CHECK_POINTEE_EQUALS(exess_get_ulong(&a_ulong), 11u);
+ CHECK_POINTEE_EQUALS(exess_get_uint(&a_uint), 12u);
+ CHECK_POINTEE_EQUALS(exess_get_ushort(&a_ushort), 13u);
+ CHECK_POINTEE_EQUALS(exess_get_ubyte(&a_ubyte), 14u);
+ assert(!memcmp(exess_get_duration(&a_duration), &duration, sizeof(duration)));
+ assert(!memcmp(exess_get_datetime(&a_datetime), &datetime, sizeof(datetime)));
+ assert(!memcmp(exess_get_time(&a_time), &time, sizeof(time)));
+ assert(!memcmp(exess_get_date(&a_date), &date, sizeof(date)));
+ assert(exess_get_blob(&a_hex)->size == sizeof(blob_data));
+ assert(exess_get_blob(&a_hex)->data == blob_data);
+ assert(exess_get_blob(&a_base64)->size == sizeof(blob_data));
+ assert(exess_get_blob(&a_base64)->data == blob_data);
+
+ // Unsuccessful get
+ assert(!exess_get_boolean(&a_int));
+ assert(!exess_get_double(&a_int));
+ assert(!exess_get_float(&a_int));
+ assert(!exess_get_long(&a_bool));
+ assert(!exess_get_int(&a_bool));
+ assert(!exess_get_short(&a_int));
+ assert(!exess_get_byte(&a_int));
+ assert(!exess_get_ulong(&a_int));
+ assert(!exess_get_uint(&a_int));
+ assert(!exess_get_ushort(&a_int));
+ assert(!exess_get_ubyte(&a_int));
+ assert(!exess_get_duration(&a_int));
+ assert(!exess_get_datetime(&a_int));
+ assert(!exess_get_time(&a_int));
+ assert(!exess_get_date(&a_int));
+ assert(!exess_get_blob(&a_int));
+}
+
+int
+main(void)
+{
+ test_read_variant();
+ test_variant_string_length();
+ test_write_variant();
+ test_make_get();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/time_test_utils.h b/subprojects/exess/test/time_test_utils.h
new file mode 100644
index 00000000..00993207
--- /dev/null
+++ b/subprojects/exess/test/time_test_utils.h
@@ -0,0 +1,52 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "int_test_data.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+
+#define INIT_ZONE(hour, minute) \
+ { \
+ 4 * (hour) + (minute) / 15 \
+ }
+
+static inline ExessTimezone
+random_timezone(uint32_t* rng)
+{
+ *rng = lcg32(*rng);
+
+ const int8_t hour = (int8_t)((*rng % 27) - 13);
+
+ *rng = lcg32(*rng);
+
+ const int8_t minute = (int8_t)((hour < 0 ? -1 : 1) * (int32_t)(*rng % 60));
+
+ const ExessTimezone zone = {(int8_t)(4 * hour + minute / 15)};
+ return zone;
+}
+
+static inline bool
+timezone_matches(const ExessTimezone zone,
+ const int8_t expected_hour,
+ const int8_t expected_minute,
+ const bool expected_is_present)
+
+{
+ return (!expected_is_present && zone.quarter_hours == EXESS_LOCAL) ||
+ zone.quarter_hours == 4 * expected_hour + expected_minute / 15;
+}