summaryrefslogtreecommitdiffhomepage
path: root/tests
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2022-01-29 23:31:16 +0100
committerJo-Philipp Wich <jo@mein.io>2022-02-03 17:22:43 +0100
commit7edad5cefa0f065aa83dffd2d7830aeaf9f38662 (patch)
tree86b727f434302ffb28cb59278243517f9765e170 /tests
parentd5003fde57eab19588da7bfdbaefe93d47435eb6 (diff)
tests: add functional tests for builtin functions
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'tests')
-rw-r--r--tests/custom/03_stdlib/01_chr27
-rw-r--r--tests/custom/03_stdlib/02_die49
-rw-r--r--tests/custom/03_stdlib/03_exists38
-rw-r--r--tests/custom/03_stdlib/04_exit36
-rw-r--r--tests/custom/03_stdlib/05_getenv30
-rw-r--r--tests/custom/03_stdlib/06_filter113
-rw-r--r--tests/custom/03_stdlib/07_hex33
-rw-r--r--tests/custom/03_stdlib/08_int42
-rw-r--r--tests/custom/03_stdlib/09_join31
-rw-r--r--tests/custom/03_stdlib/10_keys21
-rw-r--r--tests/custom/03_stdlib/11_lc27
-rw-r--r--tests/custom/03_stdlib/12_map111
-rw-r--r--tests/custom/03_stdlib/13_ord46
-rw-r--r--tests/custom/03_stdlib/14_type43
-rw-r--r--tests/custom/03_stdlib/15_reverse31
-rw-r--r--tests/custom/03_stdlib/16_sort108
-rw-r--r--tests/custom/03_stdlib/17_splice98
-rw-r--r--tests/custom/03_stdlib/18_split87
-rw-r--r--tests/custom/03_stdlib/19_substr78
-rw-r--r--tests/custom/03_stdlib/20_time18
-rw-r--r--tests/custom/03_stdlib/21_uc27
-rw-r--r--tests/custom/03_stdlib/22_uchr19
-rw-r--r--tests/custom/03_stdlib/23_values35
-rw-r--r--tests/custom/03_stdlib/24_trim46
-rw-r--r--tests/custom/03_stdlib/25_ltrim46
-rw-r--r--tests/custom/03_stdlib/26_rtrim46
-rw-r--r--tests/custom/03_stdlib/27_sprintf550
-rw-r--r--tests/custom/03_stdlib/28_printf526
-rw-r--r--tests/custom/03_stdlib/29_require164
-rw-r--r--tests/custom/03_stdlib/30_iptoarr46
-rw-r--r--tests/custom/03_stdlib/31_arrtoip57
-rw-r--r--tests/custom/03_stdlib/32_match55
-rw-r--r--tests/custom/03_stdlib/33_replace207
-rw-r--r--tests/custom/03_stdlib/34_json110
-rw-r--r--tests/custom/03_stdlib/35_include173
-rw-r--r--tests/custom/03_stdlib/36_render167
-rw-r--r--tests/custom/03_stdlib/37_warn40
-rw-r--r--tests/custom/03_stdlib/38_system148
-rw-r--r--tests/custom/03_stdlib/39_trace79
-rw-r--r--tests/custom/03_stdlib/40_proto89
-rw-r--r--tests/custom/03_stdlib/41_sleep46
-rw-r--r--tests/custom/03_stdlib/42_assert30
-rw-r--r--tests/custom/03_stdlib/43_regexp117
-rw-r--r--tests/custom/03_stdlib/44_wildcard43
-rw-r--r--tests/custom/03_stdlib/45_sourcepath69
-rw-r--r--tests/custom/03_stdlib/46_min28
-rw-r--r--tests/custom/03_stdlib/47_max28
-rw-r--r--tests/custom/03_stdlib/48_b64dec31
-rw-r--r--tests/custom/03_stdlib/49_b64enc25
-rw-r--r--tests/custom/03_stdlib/50_uniq66
50 files changed, 4180 insertions, 0 deletions
diff --git a/tests/custom/03_stdlib/01_chr b/tests/custom/03_stdlib/01_chr
new file mode 100644
index 0000000..17163e3
--- /dev/null
+++ b/tests/custom/03_stdlib/01_chr
@@ -0,0 +1,27 @@
+The `chr()` function converts each given numeric value into a character
+and returns the resulting string, e.g. passing 97, 98 and 99 will yield
+the string `abc`.
+
+Negative numeric values and values which cannot be converted to integers
+are treated as `0`, values larger than `255` are capped to `255`.
+
+The resulting string will have the same length as the amount of arguments
+passed to the `chr()` function.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ chr(),
+ chr(97, 98, 99),
+ chr(-1, false, null, [], {}, "0x41", 66.5, 1000)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "",
+ "abc",
+ "\u0000\u0000\u0000\u0000\u0000AB\u00ff"
+]
+-- End --
diff --git a/tests/custom/03_stdlib/02_die b/tests/custom/03_stdlib/02_die
new file mode 100644
index 0000000..344069a
--- /dev/null
+++ b/tests/custom/03_stdlib/02_die
@@ -0,0 +1,49 @@
+The `die()` function triggers a user defined runtime exception when invoked,
+using the given value as exception message.
+
+The given message value is converted to a string internally if it is not a
+string already. If no message argument is given or if the message argument
+is `null`, the default message is `Died`.
+
+The function does not return.
+
+-- Testcase --
+{%
+ print("Before invoking die()\n");
+
+ die("An exception!");
+
+ print("After invoking die()\n");
+%}
+-- End --
+
+-- Expect stdout --
+Before invoking die()
+-- End --
+
+-- Expect stderr --
+An exception!
+In line 4, byte 21:
+
+ ` die("An exception!");`
+ Near here -------------^
+
+
+-- End --
+
+
+-- Testcase --
+{%
+ die();
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In line 2, byte 6:
+
+ ` die();`
+ ^-- Near here
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/03_exists b/tests/custom/03_stdlib/03_exists
new file mode 100644
index 0000000..3d71aa7
--- /dev/null
+++ b/tests/custom/03_stdlib/03_exists
@@ -0,0 +1,38 @@
+The `exists()` function checks the existence of the given key within the
+given object. If the object contains the given key, `true` is returned,
+otherwise `false`.
+
+If the object argument is not an object, `false` is returned as well.
+
+The key argument is converted to a string in case it is not one already.
+
+-- Testcase --
+{%
+ let obj = {
+ "foo": true,
+ "bar": false,
+ "false": null,
+ "123": "a number"
+ };
+
+ printf("%.J\n", [
+ exists(true, "test"),
+ exists(obj, "doesnotexists"),
+ exists(obj, "foo"),
+ exists(obj, "bar"),
+ exists(obj, !true),
+ exists(obj, 123)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ false,
+ false,
+ true,
+ true,
+ true,
+ true
+]
+-- End --
diff --git a/tests/custom/03_stdlib/04_exit b/tests/custom/03_stdlib/04_exit
new file mode 100644
index 0000000..1f941d1
--- /dev/null
+++ b/tests/custom/03_stdlib/04_exit
@@ -0,0 +1,36 @@
+The `exit()` function terminates the running program with the given exit
+code or 0 in case no argument is given or if the argument cannot be
+converted to an integer.
+
+The function does not return.
+
+-- Testcase --
+{%
+ print("Before invoking exit()\n");
+
+ exit();
+
+ print("After invoking exit()\n");
+%}
+-- End --
+
+-- Expect stdout --
+Before invoking exit()
+-- End --
+
+-- Expect exitcode --
+0
+-- End --
+
+
+Passing a code argument overrides the default "0" value.
+
+-- Testcase --
+{%
+ exit(123)
+%}
+-- End --
+
+-- Expect exitcode --
+123
+-- End --
diff --git a/tests/custom/03_stdlib/05_getenv b/tests/custom/03_stdlib/05_getenv
new file mode 100644
index 0000000..350e952
--- /dev/null
+++ b/tests/custom/03_stdlib/05_getenv
@@ -0,0 +1,30 @@
+The `getenv()` function returns the value of the given environment variable
+or `null` if either the given variable does not exist or if the given name
+argument is not a string.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ getenv("TEST_VARIABLE"),
+ getenv("EMPTY_VARIABLE"),
+ getenv("THIS_LIKELY_DOES_NOT_EXIST"),
+ getenv(123),
+ getenv(null)
+ ]);
+%}
+-- End --
+
+-- Vars --
+TEST_VARIABLE=Test Value
+EMPTY_VARIABLE=
+-- End --
+
+-- Expect stdout --
+[
+ "Test Value",
+ "",
+ null,
+ null,
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/06_filter b/tests/custom/03_stdlib/06_filter
new file mode 100644
index 0000000..400db34
--- /dev/null
+++ b/tests/custom/03_stdlib/06_filter
@@ -0,0 +1,113 @@
+The `filter()` function filters the given array by invoking the specified
+callback for each item of the input array and only keeping items for which
+the callback returned a truish value.
+
+Returns the filtered copy of the input array, maintaining the original order
+of items. The input array is not modified.
+
+Returns `null` if the first argument is not an array.
+
+-- Testcase --
+{%
+ let numbers = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
+
+ printf("%.J\n",
+ filter(numbers, function(n) {
+ return (n % 2) == 0;
+ })
+ );
+%}
+-- End --
+
+-- Expect stdout --
+[
+ 0,
+ 2,
+ 4,
+ 6,
+ 8
+]
+-- End --
+
+
+Supplying an invalid callback will trigger an exception.
+
+-- Testcase --
+{%
+ filter([1, 2, 3], "not_a_function")
+%}
+-- End --
+
+-- Expect stderr --
+Type error: left-hand side is not a function
+In line 2, byte 36:
+
+ ` filter([1, 2, 3], "not_a_function")`
+ Near here ----------------------------^
+
+
+-- End --
+
+
+Supplying an invalid array will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", filter("not_an_array", function(i) { return i > 3 }));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+The callback is invoked with three argument for each item, the current item
+value, the index position of the item and the input array being mapped.
+
+-- Testcase --
+{%
+ let words = [ "foo", "bar", "baz", "qrx" ];
+
+ print(join("\n",
+ filter(words, function(word, idx, src) {
+ printf("word=%s, idx=%d, src=%J\n", word, idx, src);
+
+ return true;
+ })
+ ), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+word=foo, idx=0, src=[ "foo", "bar", "baz", "qrx" ]
+word=bar, idx=1, src=[ "foo", "bar", "baz", "qrx" ]
+word=baz, idx=2, src=[ "foo", "bar", "baz", "qrx" ]
+word=qrx, idx=3, src=[ "foo", "bar", "baz", "qrx" ]
+foo
+bar
+baz
+qrx
+-- End --
+
+
+Exceptions in the callback terminate the filter process and are
+propagated to the calling context.
+
+-- Testcase --
+{%
+ filter([ 1, 2, 3 ], function() { die() });
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In [anonymous function](), line 2, byte 39:
+ called from function filter ([C])
+ called from anonymous function ([stdin]:2:42)
+
+ ` filter([ 1, 2, 3 ], function() { die() });`
+ Near here -------------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/07_hex b/tests/custom/03_stdlib/07_hex
new file mode 100644
index 0000000..419970c
--- /dev/null
+++ b/tests/custom/03_stdlib/07_hex
@@ -0,0 +1,33 @@
+The `hex()` function converts the given hexadecimal string into a signed
+integer value and returns the resulting number.
+
+Returns `NaN` if the given argument is not a string, an empty string or
+a string containing non-hexadecimal digits.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ hex(),
+ hex(false),
+ hex(123),
+ hex(""),
+ hex("invalid"),
+ hex("deaf"),
+ hex("0x1000"),
+ hex("ffffffffffffffff")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "NaN",
+ "NaN",
+ "NaN",
+ "NaN",
+ "NaN",
+ 57007,
+ 4096,
+ 9223372036854775807
+]
+-- End --
diff --git a/tests/custom/03_stdlib/08_int b/tests/custom/03_stdlib/08_int
new file mode 100644
index 0000000..a6b5923
--- /dev/null
+++ b/tests/custom/03_stdlib/08_int
@@ -0,0 +1,42 @@
+The `int()` function converts the given value into a signed integer
+value and returns the resulting number.
+
+Returns `NaN` if the given argument is not convertible into a number.
+
+Returns `NaN` if the conversion result is out of range.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ int(),
+ int(false),
+ int(123),
+ int(456.789),
+ int(""),
+ int("invalid"),
+ int("deaf"),
+ int("0x1000"),
+ int("0xffffffffffffffff"),
+ int("0177"),
+ int("+145"),
+ int("-96")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ 0,
+ 0,
+ 123,
+ 456,
+ 0,
+ "NaN",
+ "NaN",
+ 4096,
+ "NaN",
+ 127,
+ "NaN",
+ -96
+]
+-- End --
diff --git a/tests/custom/03_stdlib/09_join b/tests/custom/03_stdlib/09_join
new file mode 100644
index 0000000..dac49c3
--- /dev/null
+++ b/tests/custom/03_stdlib/09_join
@@ -0,0 +1,31 @@
+The `join()` function constructs a string out of the given array by
+converting each array item into a string and then joining these substrings
+putting the given separator value in between. An empty array will result in
+an empty string.
+
+The separator argument is converted into a string in case it is not already
+a string value.
+
+Returns `null` if the given array argument is not an array value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ join("|", []),
+ join("|", [ 1, 2, 3 ]),
+ join("|", [ null, false, "" ]),
+ join(123, [ "a", "b", "c" ]),
+ join(123, { "not": "an", "array": "value" })
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "",
+ "1|2|3",
+ "null|false|",
+ "a123b123c",
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/10_keys b/tests/custom/03_stdlib/10_keys
new file mode 100644
index 0000000..46462a0
--- /dev/null
+++ b/tests/custom/03_stdlib/10_keys
@@ -0,0 +1,21 @@
+The `keys()` function returns an array containing all keys of the given
+dictionary value. The keys are sorted in declaration order.
+
+-- Testcase --
+{{ keys({ "foo": true, "bar": false, "qrx": 123 }) }}
+-- End --
+
+-- Expect stdout --
+[ "foo", "bar", "qrx" ]
+-- End --
+
+
+If the given argument is not a dictionary, the function returns `null`.
+
+-- Testcase --
+{{ keys(true) === null }}
+-- End --
+
+-- Expect stdout --
+true
+-- End --
diff --git a/tests/custom/03_stdlib/11_lc b/tests/custom/03_stdlib/11_lc
new file mode 100644
index 0000000..1ae3cb1
--- /dev/null
+++ b/tests/custom/03_stdlib/11_lc
@@ -0,0 +1,27 @@
+The `lc()` function turns each upper case character in the source string
+into lower case and returns the resulting copy.
+
+The input argument is converted to a string in case it is not already a
+string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ lc("This Will Be All Lowercased."),
+ lc([ "An", "array", "ABC" ]),
+ lc(123),
+ lc(false),
+ lc()
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "this will be all lowercased.",
+ "[ \"an\", \"array\", \"abc\" ]",
+ "123",
+ "false",
+ "null"
+]
+-- End --
diff --git a/tests/custom/03_stdlib/12_map b/tests/custom/03_stdlib/12_map
new file mode 100644
index 0000000..508838d
--- /dev/null
+++ b/tests/custom/03_stdlib/12_map
@@ -0,0 +1,111 @@
+The `map()` function creates a new array from the given input array by
+invoking the specified callback for each item of the input array and
+putting the resulting return value into the new array.
+
+Returns the newly created array. The input array is not modified.
+
+Returns `null` if the first argument is not an array.
+
+-- Testcase --
+{%
+ let numbers = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ];
+
+ printf("%.J\n",
+ map(numbers, function(n) {
+ return (n * n);
+ })
+ );
+%}
+-- End --
+
+-- Expect stdout --
+[
+ 0,
+ 1,
+ 4,
+ 9,
+ 16,
+ 25,
+ 36,
+ 49,
+ 64,
+ 81
+]
+-- End --
+
+
+Supplying an invalid callback will trigger an exception.
+
+-- Testcase --
+{%
+ map([1, 2, 3], "not_a_function")
+%}
+-- End --
+
+-- Expect stderr --
+Type error: left-hand side is not a function
+In line 2, byte 33:
+
+ ` map([1, 2, 3], "not_a_function")`
+ Near here -------------------------^
+
+
+-- End --
+
+
+Supplying an invalid array will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", map("not_an_array", function(i) { return i > 3 }));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+The callback is invoked with three argument for each item, the current item
+value, the index position of the item and the input array being mapped.
+
+-- Testcase --
+{%
+ let words = [ "foo", "bar", "baz", "qrx" ];
+
+ print(join("\n",
+ map(words, function(word, idx, src) {
+ return sprintf("word=%s, idx=%d, src=%J", word, idx, src);
+ })
+ ), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+word=foo, idx=0, src=[ "foo", "bar", "baz", "qrx" ]
+word=bar, idx=1, src=[ "foo", "bar", "baz", "qrx" ]
+word=baz, idx=2, src=[ "foo", "bar", "baz", "qrx" ]
+word=qrx, idx=3, src=[ "foo", "bar", "baz", "qrx" ]
+-- End --
+
+
+Exceptions in the callback terminate the map process and are
+propagated to the calling context.
+
+-- Testcase --
+{%
+ map([ 1, 2, 3 ], function() { die() });
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In [anonymous function](), line 2, byte 36:
+ called from function map ([C])
+ called from anonymous function ([stdin]:2:39)
+
+ ` map([ 1, 2, 3 ], function() { die() });`
+ Near here ----------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/13_ord b/tests/custom/03_stdlib/13_ord
new file mode 100644
index 0000000..3a69228
--- /dev/null
+++ b/tests/custom/03_stdlib/13_ord
@@ -0,0 +1,46 @@
+The `ord()` function extracts the byte values of characters within the
+given input string at different offsets, depending on the arguments.
+
+Without further arguments, the function will return the byte value of
+the first character within the given string.
+
+If one or more offset arguments are given, the function returns an array
+containing the byte values of each character at the corresponding offset.
+
+Returns `null` if the given input string argument is not a string.
+
+Returns `null` if the given input string is empty and no offset arguments
+are provided.
+
+If invalid offsets are given, the corresponding values within the result
+array will be set to `null`.
+
+Invalid offsets are non-integer values or integers equal to or larger than
+the length of the input string. Negative offsets are converted to positive
+ones by adding the length of the input string. If the negative value is
+too large, the offset is considered invalid.
+
+
+-- Testcase --
+{%
+ print(join("\n", [
+ ord(123),
+ ord(""),
+ ord("abcd"),
+ ord("abcd", 0),
+ ord("abcd", 1, 3, 2),
+ ord("abcd", -1, -2),
+ ord("abcd", -10, 10)
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+null
+null
+97
+[ 97 ]
+[ 98, 100, 99 ]
+[ 100, 99 ]
+[ null, null ]
+-- End --
diff --git a/tests/custom/03_stdlib/14_type b/tests/custom/03_stdlib/14_type
new file mode 100644
index 0000000..243618e
--- /dev/null
+++ b/tests/custom/03_stdlib/14_type
@@ -0,0 +1,43 @@
+The `type()` function returns the type name of the given argument as
+string.
+
+Returns `null` if the given argument is `null` or omitted.
+
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ type(),
+ type(null),
+ type(false),
+ type(true),
+ type(123),
+ type(-0xaf),
+ type(456.789),
+ type(-456.789),
+ type([ "foo", "bar", "baz" ]),
+ type({ example: "object" }),
+ type(function() {}),
+ type((n) => n * n),
+ type(print)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ null,
+ null,
+ "bool",
+ "bool",
+ "int",
+ "int",
+ "double",
+ "double",
+ "array",
+ "object",
+ "function",
+ "function",
+ "function"
+]
+-- End --
diff --git a/tests/custom/03_stdlib/15_reverse b/tests/custom/03_stdlib/15_reverse
new file mode 100644
index 0000000..176cd4c
--- /dev/null
+++ b/tests/custom/03_stdlib/15_reverse
@@ -0,0 +1,31 @@
+The `reverse()` function returns the input argument in reverse order.
+
+Returns a reversed copy of the input string if the given input value
+argument is a string.
+
+Returns a reversed copy of the input array if the given input value
+argument is an array.
+
+Returns `null` if the input argument is neither a string nor an array.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ reverse("abc"),
+ reverse([1, 2, 3]),
+ reverse(true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "cba",
+ [
+ 3,
+ 2,
+ 1
+ ],
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/16_sort b/tests/custom/03_stdlib/16_sort
new file mode 100644
index 0000000..ccc235f
--- /dev/null
+++ b/tests/custom/03_stdlib/16_sort
@@ -0,0 +1,108 @@
+The `sort()` function performs an in-place sorting on the given array,
+invoking the specified callback (if any) to compare items during the
+sort process.
+
+If no callback is given or if the callback argument is `null`, a default
+comparator function is used which will sort number values numerically
+and all other value types lexically.
+
+Returns the sorted input array.
+
+Returns `null` if the given input array value is not an array.
+
+
+-- Testcase --
+{%
+ print(join("\n", [
+ // default numeric sort
+ sort([ 6, 4.3, 1, 45, 3.01, 2 ]),
+
+ // default lexical sort
+ sort([ "qrx", "bar", "foo", "abc" ]),
+
+ // default lexical sort due to implicit stringification
+ sort([ true, false, null, 1, "2b" ]),
+
+ // sort with custom callback (by word length)
+ sort([ "apple", "pear", "banana", "grapefruit" ], (a, b) => length(a) - length(b)),
+
+ // sort with custom callback (by type, then value)
+ sort([ 4, 1, 9, 2, "x", "a", "q", "b" ], (a, b) => {
+ let t1 = type(a), t2 = type(b);
+ if (t1 < t2)
+ return -1;
+ else if (t2 > t2)
+ return 1;
+
+ if (a < b)
+ return -1;
+ else if (a > b)
+ return 1;
+
+ return 0;
+ })
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ 1, 2, 3.01, 4.3, 6, 45 ]
+[ "abc", "bar", "foo", "qrx" ]
+[ 1, "2b", false, null, true ]
+[ "pear", "apple", "banana", "grapefruit" ]
+[ 1, 2, 4, 9, "a", "b", "q", "x" ]
+-- End --
+
+
+Supplying an invalid callback will trigger an exception.
+
+-- Testcase --
+{%
+ sort([3, 1, 2], "not_a_function")
+%}
+-- End --
+
+-- Expect stderr --
+Type error: left-hand side is not a function
+In line 2, byte 34:
+
+ ` sort([3, 1, 2], "not_a_function")`
+ Near here --------------------------^
+
+
+-- End --
+
+
+Supplying an invalid array will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", sort("not_an_array", function(a, b) { return a - b }));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+Exceptions in the callback terminate the sort process and are
+propagated to the calling context.
+
+-- Testcase --
+{%
+ sort([ 1, 2, 3 ], function() { die() });
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In [anonymous function](), line 2, byte 37:
+ called from function sort ([C])
+ called from anonymous function ([stdin]:2:40)
+
+ ` sort([ 1, 2, 3 ], function() { die() });`
+ Near here -----------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/17_splice b/tests/custom/03_stdlib/17_splice
new file mode 100644
index 0000000..f8fb9a2
--- /dev/null
+++ b/tests/custom/03_stdlib/17_splice
@@ -0,0 +1,98 @@
+The `splice()` function performs in-place addition and removal of elements
+on the given array.
+
+If no offset, remove count and additional items are supplied, all elements
+are removed from the array.
+
+If just an offset, but no remove count and not additional items are given,
+all elements beginning with the given offset until the end of the array
+are removed.
+
+If at least an offset and a remove count are given, then that amount of
+items are removed from the array, beginning at the specified offset. Any
+further supplied additional item (if any) is inserted in the same order
+beginning at the given offset.
+
+If either the offset or the remove count are negative, they're treated
+as counting towards the end of the array. If either value exceeds the
+array length, it is capped to the length of the array.
+
+Returns the modified input array.
+
+Returns `null` if the given input array value is not an array.
+
+
+-- Testcase --
+{%
+ let arr = [ 6, 4.3, 1, 45, 3.01, 2 ];
+
+ print(join("\n", [
+ // remove all items
+ splice([ ...arr ]),
+
+ // remove all items from index 4 till end
+ splice([ ...arr ], 4),
+
+ // remove item 2 and 3
+ splice([ ...arr ], 1, 2),
+
+ // remove last two items
+ splice([ ...arr ], -2),
+
+ // remove items 4 and 5
+ splice([ ...arr ], -3, -1),
+
+ // replace item 2
+ splice([ ...arr ], 1, 1, 7.9),
+
+ // add item between 3 and 4
+ splice([ ...arr ], 3, 0, 34),
+
+ // append three items
+ splice([ ...arr ], length(arr), 0, 123, 456, 789)
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ ]
+[ 6, 4.3, 1, 45 ]
+[ 6, 45, 3.01, 2 ]
+[ 6, 4.3, 1, 45 ]
+[ 6, 4.3, 1, 2 ]
+[ 6, 7.9, 1, 45, 3.01, 2 ]
+[ 6, 4.3, 1, 34, 45, 3.01, 2 ]
+[ 6, 4.3, 1, 45, 3.01, 2, 123, 456, 789 ]
+-- End --
+
+
+Supplying an invalid array will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", splice("not_an_array", 0, 1));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+Invalid, non-numeric offset or index values are treated as 0.
+
+-- Testcase --
+{%
+ let arr = [ 6, 4.3, 1, 45, 3.01, 2 ];
+
+ print(join("\n", [
+ splice([ ...arr ], "foo", "bar"),
+ splice([ ...arr ], "foo", "bar", "baz")
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ 6, 4.3, 1, 45, 3.01, 2 ]
+[ "baz", 6, 4.3, 1, 45, 3.01, 2 ]
+-- End --
diff --git a/tests/custom/03_stdlib/18_split b/tests/custom/03_stdlib/18_split
new file mode 100644
index 0000000..e31ce78
--- /dev/null
+++ b/tests/custom/03_stdlib/18_split
@@ -0,0 +1,87 @@
+The `split()` function breaks the given string into multiple substrings,
+using the given separator value.
+
+The separator may be either a string or a regular expression value.
+
+Returns an array containing the resulting parts.
+
+Returns `null` if the given input value is not a string or if the separator
+argument is neither a string nor a regular expression.
+
+
+-- Testcase --
+{%
+ print(join("\n", [
+ // split by string
+ split("foo|bar|baz", "|"),
+
+ // split by regexp
+ split("apples, bananas and strawberries are fruits", /, | and | are | /),
+
+ // splitting an empty string yields an array containing one empty string
+ split("", "|"),
+ split("", ""),
+ split("", /\s+/),
+
+ // splitting with an empty string as separator yields an array containing
+ // all characters individually
+ split("foo|bar|baz", ""),
+ split("foo|bar|baz", /()/),
+
+ // splitting on a separator not found within the string will yield an
+ // array containing the entire string as sole element
+ split("foo|bar|baz", "xxx"),
+ split("foo|bar|baz", /\d+/),
+
+ // subsequent separators are not coalesced
+ split("abc|||def", "|"),
+ split("foo1bar23baz", /[[:digit:]]/),
+
+ // leading and trailing empty substrings are retained
+ split("|abc|def|", "|"),
+ split(",foo;bar:", /[,;:]/),
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ "foo", "bar", "baz" ]
+[ "apples", "bananas", "strawberries", "fruits" ]
+[ "" ]
+[ "" ]
+[ "" ]
+[ "f", "o", "o", "|", "b", "a", "r", "|", "b", "a", "z" ]
+[ "f", "o", "o", "|", "b", "a", "r", "|", "b", "a", "z" ]
+[ "foo|bar|baz" ]
+[ "foo|bar|baz" ]
+[ "abc", "", "", "def" ]
+[ "foo", "bar", "", "baz" ]
+[ "", "abc", "def", "" ]
+[ "", "foo", "bar", "" ]
+-- End --
+
+
+Supplying an invalid input string value will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", split(true, "u"));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
+
+
+Supplying a non-string, non-regexp separator will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", split("null true false", true));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/19_substr b/tests/custom/03_stdlib/19_substr
new file mode 100644
index 0000000..0c7eed3
--- /dev/null
+++ b/tests/custom/03_stdlib/19_substr
@@ -0,0 +1,78 @@
+The `substr()` function extracts a portion of the given input string,
+specified by offset and length. and returns the resulting substring.
+
+If neither an offset, nor a length argument are provided, a copy of
+the entire input string is returned.
+
+If just an offset is specified, the entire remainder of the input string
+after the specified offset is returned.
+
+If both an offset and a length are specified, then that much characters
+of the string are extracted, beginning at the offset.
+
+If either offset or length are negative, they're counted towards the end
+of the string. If either value exceeds the input string length, it is
+capped to the length.
+
+Returns the resulting substring.
+
+Returns `null` if the given input value is not a string.
+
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // extract entire string
+ substr("Hello world!"),
+
+ // extract anything after the 3rd character
+ substr("Hello world!", 3),
+
+ // extract the last 6 characters
+ substr("Hello world!", -6),
+
+ // extract chars 5-8
+ substr("Hello world!", 4, 3),
+
+ // extract characters 8-10
+ substr("Hello world!", -5, -2),
+
+ // overlong values are capped
+ substr("Hello world!", 100),
+ substr("Hello world!", 0, 100),
+ substr("Hello world!", 100, 100),
+
+ // invalid offset or length values are treated as 0
+ substr("Hello world!", "inval"),
+ substr("Hello world!", "inval", "inval")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello world!",
+ "lo world!",
+ "world!",
+ "o w",
+ "orl",
+ "",
+ "Hello world!",
+ "",
+ "Hello world!",
+ ""
+]
+-- End --
+
+
+Supplying an invalid input string value will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", substr(true, 0, 1));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/20_time b/tests/custom/03_stdlib/20_time
new file mode 100644
index 0000000..173ae09
--- /dev/null
+++ b/tests/custom/03_stdlib/20_time
@@ -0,0 +1,18 @@
+The `time()` function returns the current UNIX epoch time.
+
+
+-- Testcase --
+{%
+ let timestamp = time();
+ let testcmd = sprintf('t=$(date +%%s); [ $t -gt %d -a $t -lt %d ]', timestamp - 3, timestamp + 3);
+
+ if (system(testcmd) == 0)
+ print("time() works\n");
+ else
+ print("time() and `date +%s` yield different results!\n");
+%}
+-- End --
+
+-- Expect stdout --
+time() works
+-- End --
diff --git a/tests/custom/03_stdlib/21_uc b/tests/custom/03_stdlib/21_uc
new file mode 100644
index 0000000..a5aeed3
--- /dev/null
+++ b/tests/custom/03_stdlib/21_uc
@@ -0,0 +1,27 @@
+The `uc()` function turns each lower case character in the source string
+into upper case and returns the resulting copy.
+
+The input argument is converted to a string in case it is not already a
+string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ uc("This Will Be All Uppercased."),
+ uc([ "An", "array", "ABC" ]),
+ uc(123),
+ uc(false),
+ uc()
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "THIS WILL BE ALL UPPERCASED.",
+ "[ \"AN\", \"ARRAY\", \"ABC\" ]",
+ "123",
+ "FALSE",
+ "NULL"
+]
+-- End --
diff --git a/tests/custom/03_stdlib/22_uchr b/tests/custom/03_stdlib/22_uchr
new file mode 100644
index 0000000..8f24f3a
--- /dev/null
+++ b/tests/custom/03_stdlib/22_uchr
@@ -0,0 +1,19 @@
+The `uchr()` function takes a series of code point values and turns them
+into an UTF-8 encoded string. The resulting string will have as many
+characters as there were arguments to the function. The number of bytes
+per character varies between 1 to 4, depending on the code point value.
+
+Invalid numeric arguments or arguments being out of range 0-0x10FFFF will
+be encoded as the Unicode replacement character 0xFFFD.
+
+Returns the resulting UTF-8 string.
+
+-- Testcase --
+{{ uchr(0x2600, 0x2601, 0x2602) }}
+{{ uchr("inval", -1, 0xffffffff) }}
+-- End --
+
+-- Expect stdout --
+☀☁☂
+���
+-- End --
diff --git a/tests/custom/03_stdlib/23_values b/tests/custom/03_stdlib/23_values
new file mode 100644
index 0000000..34b600e
--- /dev/null
+++ b/tests/custom/03_stdlib/23_values
@@ -0,0 +1,35 @@
+The `values()` extracts all values of a given dictionary. The values in the
+resulting array are ordered according to the keys which in turn follow
+declaration or assignment order.
+
+Returns an array containg the value of each key within the given dictionary
+value.
+
+Returns `null` if the given dictionary argment is not a valid dictionary.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ values({ foo: true, bar: false, baz: null, qrx: 123, xyz: "test" }),
+ values({}),
+ values(true),
+ values()
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ [
+ true,
+ false,
+ null,
+ 123,
+ "test"
+ ],
+ [
+ ],
+ null,
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/24_trim b/tests/custom/03_stdlib/24_trim
new file mode 100644
index 0000000..263fd18
--- /dev/null
+++ b/tests/custom/03_stdlib/24_trim
@@ -0,0 +1,46 @@
+The `trim()` function removes specific leading and trailing characters from
+a given input string. If the characters to trim are unspecified, the space, tab,
+carriage return and newline characters will be used by default.
+
+Returns a copy of the input string with the specified leading and trailing
+characters removed.
+
+Returns `null` if the given input argment is not a valid string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // not specifying trim characters will trim whitespace
+ trim(" Hello World! \r\n"),
+
+ // if trim characters are specified, only those are removed
+ trim("|* Foo Bar +|", "+*|"),
+
+ // trim does not affect characters in the middle of the string
+ trim(" Foo Bar "),
+ trim("|Foo|Bar|", "|")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello World!",
+ " Foo Bar ",
+ "Foo Bar",
+ "Foo|Bar"
+]
+-- End --
+
+
+Supplying an invalid string will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", trim(true));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/25_ltrim b/tests/custom/03_stdlib/25_ltrim
new file mode 100644
index 0000000..2001322
--- /dev/null
+++ b/tests/custom/03_stdlib/25_ltrim
@@ -0,0 +1,46 @@
+The `ltrim()` function removes specific leading characters from the given
+input string. If the characters to trim are unspecified, the space, tab,
+carriage return and newline characters will be used by default.
+
+Returns a copy of the input string with the specified leading characters
+removed.
+
+Returns `null` if the given input argment is not a valid string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // not specifying trim characters will trim whitespace
+ ltrim(" Hello World!"),
+
+ // if trim characters are specified, only those are removed
+ ltrim("|* Foo Bar +|", "+*|"),
+
+ // ltrim does not affect characters in the middle or the end
+ ltrim(" Foo Bar "),
+ ltrim("|Foo|Bar|", "|")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello World!",
+ " Foo Bar +|",
+ "Foo Bar ",
+ "Foo|Bar|"
+]
+-- End --
+
+
+Supplying an invalid string will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", ltrim(true));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/26_rtrim b/tests/custom/03_stdlib/26_rtrim
new file mode 100644
index 0000000..17b54f7
--- /dev/null
+++ b/tests/custom/03_stdlib/26_rtrim
@@ -0,0 +1,46 @@
+The `rtrim()` function removes specific trailing characters from the given
+input string. If the characters to trim are unspecified, the space, tab,
+carriage return and newline characters will be used by default.
+
+Returns a copy of the input string with the specified trailing characters
+removed.
+
+Returns `null` if the given input argment is not a valid string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // not specifying trim characters will trim whitespace
+ rtrim("Hello World! \r \n"),
+
+ // if trim characters are specified, only those are removed
+ rtrim("|* Foo Bar +|", "+*|"),
+
+ // rtrim does not affect characters in the middle or the beginning
+ rtrim(" Foo Bar "),
+ rtrim("|Foo|Bar|", "|")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello World!",
+ "|* Foo Bar ",
+ " Foo Bar",
+ "|Foo|Bar"
+]
+-- End --
+
+
+Supplying an invalid string will yield `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", ltrim(true));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/27_sprintf b/tests/custom/03_stdlib/27_sprintf
new file mode 100644
index 0000000..3edcd48
--- /dev/null
+++ b/tests/custom/03_stdlib/27_sprintf
@@ -0,0 +1,550 @@
+The `sprintf()` function formats given input value according to a format
+string specified as first argument. The format string mimicks the syntax
+and directives used by C printf().
+
+Each directive (with the exception of %%) in the format string expects
+a corresponding argument. If fewer arguments are passed to sprintf() than
+required by the format string, missing values will be assumed to be `null`
+and be interpreted accordingly. Excess arguments are ignored.
+
+Returns an output string formatted according to the format string with all
+format directives interpolated by their respective values.
+
+Returns an empty string in case the format string argument is not a valid
+string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // String interpolation, corresponding value will be converted
+ // into string if needed.
+ sprintf("Hello %s!", "World"),
+ sprintf("Hello %s!", false),
+ sprintf("Hello %s!", 123),
+ sprintf("Hello %s!", null),
+ sprintf("Hello %s!"),
+
+ // Signed integer interpolation, corresponding value will be
+ // converted into integer if needed. Also `d` and `i` are aliases.
+ sprintf("%d", 123),
+ sprintf("%i", 456.789),
+ sprintf("%d", true),
+ sprintf("%d", "0x42"),
+ sprintf("%d", "invalid"),
+ sprintf("%d", null),
+ sprintf("%d", 0xffffffffffffffff),
+ sprintf("%d"),
+
+ // Unsigned integer interpolation in decimal notation, corresponding
+ // value will be converted into unsigned integer if needed.
+ sprintf("%u", 123),
+ sprintf("%u", -123),
+ sprintf("%u", 0xffffffffffffffff),
+ sprintf("%u", 456.789),
+ sprintf("%u", "invalid"),
+ sprintf("%u", null),
+ sprintf("%u"),
+
+ // Unsigned integer interpolation in octal notation, corresponding
+ // value will be converted into unsigned integer if needed.
+ sprintf("%o", 123),
+ sprintf("%o", -123),
+ sprintf("%o", 0xffffffffffffffff),
+ sprintf("%o", 456.789),
+ sprintf("%o", "invalid"),
+ sprintf("%o", null),
+ sprintf("%o"),
+
+ // Unsigned integer interpolation in lower case hexadecimal notation,
+ // corresponding value will be converted into unsigned integer if
+ // needed.
+ sprintf("%x", 123),
+ sprintf("%x", -123),
+ sprintf("%x", 0xffffffffffffffff),
+ sprintf("%x", 456.789),
+ sprintf("%x", "invalid"),
+ sprintf("%x", null),
+ sprintf("%x"),
+
+ // Unsigned integer interpolation in upper case hexadecimal notation,
+ // corresponding value will be converted into unsigned integer if
+ // needed.
+ sprintf("%X", 123),
+ sprintf("%X", -123),
+ sprintf("%X", 0xffffffffffffffff),
+ sprintf("%X", 456.789),
+ sprintf("%X", "invalid"),
+ sprintf("%X", null),
+ sprintf("%X"),
+
+ // Floating point value interpolation in exponential notation,
+ // corresponding value will be converted to double if needed.
+ sprintf("%e", 123),
+ sprintf("%e", -123),
+ sprintf("%e", 456.789),
+ sprintf("%e", -456.789),
+ sprintf("%e", "invalid"),
+ sprintf("%e", null),
+ sprintf("%e"),
+
+ // Floating point value interpolation in exponential notation,
+ // using uppercase characters. Corresponding value will be converted
+ // to double if needed.
+ sprintf("%E", 123),
+ sprintf("%E", -123),
+ sprintf("%E", 456.789),
+ sprintf("%E", -456.789),
+ sprintf("%E", "invalid"),
+ sprintf("%E", null),
+ sprintf("%E"),
+
+ // Floating point value interpolation in decimal point notation,
+ // corresponding value will be converted to double if needed.
+ sprintf("%f", 123),
+ sprintf("%f", -123),
+ sprintf("%f", 456.789),
+ sprintf("%f", -456.789),
+ sprintf("%f", "invalid"),
+ sprintf("%f", null),
+ sprintf("%f"),
+
+ // Floating point value interpolation in decimal point notation,
+ // using uppercase characters. Corresponding value will be converted
+ // to double if needed.
+ sprintf("%F", 123),
+ sprintf("%F", -123),
+ sprintf("%F", 456.789),
+ sprintf("%F", -456.789),
+ sprintf("%F", "invalid"),
+ sprintf("%F", null),
+ sprintf("%F"),
+
+ // Floating point value interpolation in either decimal point or
+ // exponential notation, depending on size of exponent. Corresponding
+ // value will be converted to double if needed.
+ sprintf("%g", 123.456),
+ sprintf("%g", 0.0000001),
+ sprintf("%g", "invalid"),
+
+ // Floating point value interpolation in either decimal point or
+ // exponential notation, depending on size of exponent and using
+ // uppercase characters. Corresponding value will be converted to
+ // double if needed.
+ sprintf("%G", 123.456),
+ sprintf("%G", 0.0000001),
+ sprintf("%G", "invalid"),
+
+ // Character interpolation. The corresponding value is casted as `char`
+ // and the resulting character is interpolated.
+ sprintf("%c", 65),
+ sprintf("%c", -1),
+ sprintf("%c", 456.789),
+ sprintf("%c", "invalid"),
+
+ // JSON interpolation. The corresponding value is JSON encoded and
+ // interpolated as string.
+ sprintf("%J", "Hello\n"),
+ sprintf("%J", 123),
+ sprintf("%J", [ 1, 2, 3 ]),
+ sprintf("%J", { some: "dictionary", an: [ "array", true, false ] }),
+ sprintf("%J", null),
+ sprintf("%J"),
+
+ // Escaping `%`. The `%%` format string will produce a literal `%`.
+ // No corresponding argument is expected.
+ sprintf("%%")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello World!",
+ "Hello false!",
+ "Hello 123!",
+ "Hello (null)!",
+ "Hello (null)!",
+ "123",
+ "456",
+ "1",
+ "66",
+ "0",
+ "0",
+ "-1",
+ "0",
+ "123",
+ "18446744073709551493",
+ "18446744073709551615",
+ "456",
+ "0",
+ "0",
+ "0",
+ "173",
+ "1777777777777777777605",
+ "1777777777777777777777",
+ "710",
+ "0",
+ "0",
+ "0",
+ "7b",
+ "ffffffffffffff85",
+ "ffffffffffffffff",
+ "1c8",
+ "0",
+ "0",
+ "0",
+ "7B",
+ "FFFFFFFFFFFFFF85",
+ "FFFFFFFFFFFFFFFF",
+ "1C8",
+ "0",
+ "0",
+ "0",
+ "1.230000e+02",
+ "-1.230000e+02",
+ "4.567890e+02",
+ "-4.567890e+02",
+ "nan",
+ "0.000000e+00",
+ "0.000000e+00",
+ "1.230000E+02",
+ "-1.230000E+02",
+ "4.567890E+02",
+ "-4.567890E+02",
+ "NAN",
+ "0.000000E+00",
+ "0.000000E+00",
+ "123.000000",
+ "-123.000000",
+ "456.789000",
+ "-456.789000",
+ "nan",
+ "0.000000",
+ "0.000000",
+ "123.000000",
+ "-123.000000",
+ "456.789000",
+ "-456.789000",
+ "NAN",
+ "0.000000",
+ "0.000000",
+ "123.456",
+ "1e-07",
+ "nan",
+ "123.456",
+ "1E-07",
+ "NAN",
+ "A",
+ "\u00ff",
+ "\u00c8",
+ "\u0000",
+ "\"Hello\\n\"",
+ "123",
+ "[ 1, 2, 3 ]",
+ "{ \"some\": \"dictionary\", \"an\": [ \"array\", true, false ] }",
+ "null",
+ "null",
+ "%"
+]
+-- End --
+
+
+Field widths may be specified for format directives.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // by default the output of a format directive is as long as the
+ // string representation of the corresponding value
+ sprintf("[%s]", "test"),
+
+ // by specifying a field width, the output will be padded to the
+ // given length
+ sprintf("[%10s]", "test"),
+
+ // the same applies to numbers
+ sprintf("[%10d]", 123),
+ sprintf("[%10f]", 1.0),
+
+ // and to char formats
+ sprintf("[%10c]", 65),
+
+ // field width is not applicable to `%` formats
+ sprintf("[%10%]")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "[test]",
+ "[ test]",
+ "[ 123]",
+ "[ 1.000000]",
+ "[ A]",
+ "[%]"
+]
+-- End --
+
+
+Precisions may be specified for format directives.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ // For `f`, `F`, `e` and `E`, the precision specifies the amount of
+ // digits after the comma
+ sprintf("[%.3f]", 1/3),
+ sprintf("[%.3F]", 1/3),
+ sprintf("[%.3e]", 1/3),
+ sprintf("[%.3E]", 1/3),
+
+ // For `g` and `G` the precision specifies the number of significant
+ // digits to print before switching to exponential notation
+ sprintf("[%.3g]", 1000.1),
+ sprintf("[%.3G]", 1000.1),
+
+ // For strings, the precision specifies the amount of characters to
+ // print at most
+ sprintf("[%.5s]", "test"),
+ sprintf("[%.3s]", "test"),
+
+ // For JSON format, the precision specifies the amount of indentation
+ // to use. Omitting precision will not indent, specifying a precision
+ // of `0` uses tabs for indentation, any other precision uses this
+ // many spaces
+ sprintf("<%J>", [ 1, 2, 3, { true: false } ]), // no indent
+ sprintf("<%.J>", [ 1, 2, 3, { true: false } ]), // tab indent
+ sprintf("<%.0J>", [ 1, 2, 3, { true: false } ]), // tab indent
+ sprintf("<%.1J>", [ 1, 2, 3, { true: false } ]), // indent using one space
+ sprintf("<%.4J>", [ 1, 2, 3, { true: false } ]), // indent using four spaces
+
+ // precision does not apply to char, integer or `%` formats
+ sprintf("[%.3d]", 1000),
+ sprintf("[%.3c]", 65),
+ sprintf("[%.3%]"),
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[0.000]
+[0.000]
+[0.000e+00]
+[0.000E+00]
+[1e+03]
+[1E+03]
+[test]
+[tes]
+<[ 1, 2, 3, { "true": false } ]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+[1000]
+[A]
+[%]
+-- End --
+
+
+A number of flag characters are supported for format directives.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // The recognized flag characters are `#`, `0`, `-`, `+` and ` ` (space)
+ sprintf("%#0+- s", "test"),
+
+ // Repetitions of flag characters are accepted
+ sprintf("%###s", "test"),
+ sprintf("%000s", "test"),
+ sprintf("%++-s", "test"),
+ sprintf("%-- s", "test"),
+ sprintf("% s", "test"),
+
+ // The `#` flag produces alternative forms of various conversions
+ sprintf("%o / %#o", 15, 15),
+ sprintf("%x / %#x", 16, 16),
+ sprintf("%X / %#X", 17, 17),
+ sprintf("%g / %#g", 1.0, 1.0),
+
+ // The `0` flag indicates zero- instead of space-padding for various
+ // numeric conversions.
+ sprintf("%5d / %05d", -10, -10),
+ sprintf("%5d / %05d", 11, 11),
+ sprintf("%5g / %05g", -12.0, -12.0),
+ sprintf("%5g / %05g", 13.0, 13.0),
+ sprintf("%5s / %05s", "a", "a"),
+
+ // The `-` flag indicates left, instead of right padding. It will
+ // override `0` and always pad with spaces
+ sprintf("%-5d / %-05d", -10, -10),
+ sprintf("%-5d / %-05d", 11, 11),
+ sprintf("%-5g / %-05g", -12.0, -12.0),
+ sprintf("%-5g / %-05g", 13.0, 13.0),
+ sprintf("%-5s / %-05s", "a", "a"),
+
+ // The `+` flag indicates that a sign (`+` or `-`) should be placed
+ // before signed numeric values. It overrides ` ` (space).
+ sprintf("%+5d / %+05d", -10, -10),
+ sprintf("%+5d / %+05d", 11, 11),
+ sprintf("%+5g / %+05g", -12.0, -12.0),
+ sprintf("%+5g / %+05g", 13.0, 13.0),
+ sprintf("%+5s / %+05s", "a", "a"),
+
+ // The ` ` (space) flag indicates that a blank should be placed
+ // before positive numbers (useful to ensure that negative and
+ // positive values in output are aligned)
+ sprintf("%-5d / %- 5d", -10, -10),
+ sprintf("%-5d / %- 5d", 11, 11),
+ sprintf("%-5g / %- 5g", -12.0, -12.0),
+ sprintf("%-5g / %- 5g", 13.0, 13.0),
+ sprintf("%-5s / %- 5s", "a", "a"),
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "test",
+ "test",
+ "test",
+ "test",
+ "test",
+ "test",
+ "17 / 017",
+ "10 / 0x10",
+ "11 / 0X11",
+ "1 / 1.00000",
+ " -10 / -0010",
+ " 11 / 00011",
+ " -12 / -0012",
+ " 13 / 00013",
+ " a / a",
+ "-10 / -10 ",
+ "11 / 11 ",
+ "-12 / -12 ",
+ "13 / 13 ",
+ "a / a ",
+ " -10 / -0010",
+ " +11 / +0011",
+ " -12 / -0012",
+ " +13 / +0013",
+ " a / a",
+ "-10 / -10 ",
+ "11 / 11 ",
+ "-12 / -12 ",
+ "13 / 13 ",
+ "a / a "
+]
+-- End --
+
+
+Unrecognized format directives are copied to the output string as-is.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // A truncated format directive is preserved
+ sprintf("test %", "test"),
+ sprintf("test %-010.3", "test"),
+
+ // An unrecognized format directive is preserved
+ sprintf("test %y test", 123),
+ sprintf("test %~123s test", 123)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "test %",
+ "test %-010.3",
+ "test %y test",
+ "test %~123s test"
+]
+-- End --
+
+
+Missing values for format directives are treated as `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ sprintf("%s"),
+ sprintf("%d"),
+ sprintf("%u"),
+ sprintf("%o"),
+ sprintf("%x"),
+ sprintf("%X"),
+ sprintf("%f"),
+ sprintf("%F"),
+ sprintf("%e"),
+ sprintf("%E"),
+ sprintf("%g"),
+ sprintf("%G"),
+ sprintf("%c"),
+ sprintf("%J")
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "(null)",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0",
+ "0.000000",
+ "0.000000",
+ "0.000000e+00",
+ "0.000000E+00",
+ "0",
+ "0",
+ "\u0000",
+ "null"
+]
+-- End --
+
+
+Supplying a non-string format value will yield an empty string result.
+
+-- Testcase --
+{%
+ printf("%.J\n", sprintf(true, 1, 2, 3));
+%}
+-- End --
+
+-- Expect stdout --
+""
+-- End --
diff --git a/tests/custom/03_stdlib/28_printf b/tests/custom/03_stdlib/28_printf
new file mode 100644
index 0000000..a2a6d27
--- /dev/null
+++ b/tests/custom/03_stdlib/28_printf
@@ -0,0 +1,526 @@
+The `printf()` function formats given input value according to a format
+string specified as first argument. The format string mimicks the syntax
+and directives used by C printf().
+
+Each directive (with the exception of %%) in the format string expects
+a corresponding argument. If fewer arguments are passed to printf() than
+required by the format string, missing values will be assumed to be `null`
+and be interpreted accordingly. Excess arguments are ignored.
+
+Writes the output string formatted according to the format string with all
+format directives interpolated by their respective values to the standard
+output stream of the VM.
+
+Returns the number of bytes written to the output stream.
+
+Writes an empty string in case the format string argument is not a valid
+string value.
+
+-- Testcase --
+{%
+ // String interpolation, corresponding value will be converted
+ // into string if needed.
+ printf("Hello %s!\n", "World");
+ printf("Hello %s!\n", false);
+ printf("Hello %s!\n", 123);
+ printf("Hello %s!\n", null);
+ printf("Hello %s!\n");
+
+ // Signed integer interpolation, corresponding value will be
+ // converted into integer if needed. Also `d` and `i` are aliases.
+ printf("%d\n", 123);
+ printf("%i\n", 456.789);
+ printf("%d\n", true);
+ printf("%d\n", "0x42");
+ printf("%d\n", "invalid");
+ printf("%d\n", null);
+ printf("%d\n", 0xffffffffffffffff);
+ printf("%d\n");
+
+ // Unsigned integer interpolation in decimal notation, corresponding
+ // value will be converted into unsigned integer if needed.
+ printf("%u\n", 123);
+ printf("%u\n", -123);
+ printf("%u\n", 0xffffffffffffffff);
+ printf("%u\n", 456.789);
+ printf("%u\n", "invalid");
+ printf("%u\n", null);
+ printf("%u\n");
+
+ // Unsigned integer interpolation in octal notation, corresponding
+ // value will be converted into unsigned integer if needed.
+ printf("%o\n", 123);
+ printf("%o\n", -123);
+ printf("%o\n", 0xffffffffffffffff);
+ printf("%o\n", 456.789);
+ printf("%o\n", "invalid");
+ printf("%o\n", null);
+ printf("%o\n");
+
+ // Unsigned integer interpolation in lower case hexadecimal notation,
+ // corresponding value will be converted into unsigned integer if
+ // needed.
+ printf("%x\n", 123);
+ printf("%x\n", -123);
+ printf("%x\n", 0xffffffffffffffff);
+ printf("%x\n", 456.789);
+ printf("%x\n", "invalid");
+ printf("%x\n", null);
+ printf("%x\n");
+
+ // Unsigned integer interpolation in upper case hexadecimal notation,
+ // corresponding value will be converted into unsigned integer if
+ // needed.
+ printf("%X\n", 123);
+ printf("%X\n", -123);
+ printf("%X\n", 0xffffffffffffffff);
+ printf("%X\n", 456.789);
+ printf("%X\n", "invalid");
+ printf("%X\n", null);
+ printf("%X\n");
+
+ // Floating point value interpolation in exponential notation,
+ // corresponding value will be converted to double if needed.
+ printf("%e\n", 123);
+ printf("%e\n", -123);
+ printf("%e\n", 456.789);
+ printf("%e\n", -456.789);
+ printf("%e\n", "invalid");
+ printf("%e\n", null);
+ printf("%e\n");
+
+ // Floating point value interpolation in exponential notation,
+ // using uppercase characters. Corresponding value will be converted
+ // to double if needed.
+ printf("%E\n", 123);
+ printf("%E\n", -123);
+ printf("%E\n", 456.789);
+ printf("%E\n", -456.789);
+ printf("%E\n", "invalid");
+ printf("%E\n", null);
+ printf("%E\n");
+
+ // Floating point value interpolation in decimal point notation,
+ // corresponding value will be converted to double if needed.
+ printf("%f\n", 123);
+ printf("%f\n", -123);
+ printf("%f\n", 456.789);
+ printf("%f\n", -456.789);
+ printf("%f\n", "invalid");
+ printf("%f\n", null);
+ printf("%f\n");
+
+ // Floating point value interpolation in decimal point notation,
+ // using uppercase characters. Corresponding value will be converted
+ // to double if needed.
+ printf("%F\n", 123);
+ printf("%F\n", -123);
+ printf("%F\n", 456.789);
+ printf("%F\n", -456.789);
+ printf("%F\n", "invalid");
+ printf("%F\n", null);
+ printf("%F\n");
+
+ // Floating point value interpolation in either decimal point or
+ // exponential notation, depending on size of exponent. Corresponding
+ // value will be converted to double if needed.
+ printf("%g\n", 123.456);
+ printf("%g\n", 0.0000001);
+ printf("%g\n", "invalid");
+
+ // Floating point value interpolation in either decimal point or
+ // exponential notation, depending on size of exponent and using
+ // uppercase characters. Corresponding value will be converted to
+ // double if needed.
+ printf("%G\n", 123.456);
+ printf("%G\n", 0.0000001);
+ printf("%G\n", "invalid");
+
+ // Character interpolation. The corresponding value is casted as `char`
+ // and the resulting character is interpolated.
+ printf("%c\n", 65);
+ //printf("%c\n", -1);
+ //printf("%c\n", 456.789);
+ //printf("%c\n", "invalid");
+
+ // JSON interpolation. The corresponding value is JSON encoded and
+ // interpolated as string.
+ printf("%J\n", "Hello\n");
+ printf("%J\n", 123);
+ printf("%J\n", [ 1, 2, 3 ]);
+ printf("%J\n", { some: "dictionary", an: [ "array", true, false ] });
+ printf("%J\n", null);
+ printf("%J\n");
+
+ // Escaping `%`. The `%%` format string will produce a literal `%`.
+ // No corresponding argument is expected.
+ printf("%%\n");
+%}
+-- End --
+
+-- Expect stdout --
+Hello World!
+Hello false!
+Hello 123!
+Hello (null)!
+Hello (null)!
+123
+456
+1
+66
+0
+0
+-1
+0
+123
+18446744073709551493
+18446744073709551615
+456
+0
+0
+0
+173
+1777777777777777777605
+1777777777777777777777
+710
+0
+0
+0
+7b
+ffffffffffffff85
+ffffffffffffffff
+1c8
+0
+0
+0
+7B
+FFFFFFFFFFFFFF85
+FFFFFFFFFFFFFFFF
+1C8
+0
+0
+0
+1.230000e+02
+-1.230000e+02
+4.567890e+02
+-4.567890e+02
+nan
+0.000000e+00
+0.000000e+00
+1.230000E+02
+-1.230000E+02
+4.567890E+02
+-4.567890E+02
+NAN
+0.000000E+00
+0.000000E+00
+123.000000
+-123.000000
+456.789000
+-456.789000
+nan
+0.000000
+0.000000
+123.000000
+-123.000000
+456.789000
+-456.789000
+NAN
+0.000000
+0.000000
+123.456
+1e-07
+nan
+123.456
+1E-07
+NAN
+A
+"Hello\n"
+123
+[ 1, 2, 3 ]
+{ "some": "dictionary", "an": [ "array", true, false ] }
+null
+null
+%
+-- End --
+
+
+Field widths may be specified for format directives.
+
+-- Testcase --
+{%
+ // by default the output of a format directive is as long as the
+ // string representation of the corresponding value
+ printf("[%s]\n", "test");
+
+ // by specifying a field width, the output will be padded to the
+ // given length
+ printf("[%10s]\n", "test");
+
+ // the same applies to numbers
+ printf("[%10d]\n", 123);
+ printf("[%10f]\n", 1.0);
+
+ // and to char formats
+ printf("[%10c]\n", 65);
+
+ // field width is not applicable to `%` formats
+ printf("[%10%]\n");
+%}
+-- End --
+
+-- Expect stdout --
+[test]
+[ test]
+[ 123]
+[ 1.000000]
+[ A]
+[%]
+-- End --
+
+
+Precisions may be specified for format directives.
+
+-- Testcase --
+{%
+ // For `f`, `F`, `e` and `E`, the precision specifies the amount of
+ // digits after the comma
+ printf("[%.3f]\n", 1/3);
+ printf("[%.3F]\n", 1/3);
+ printf("[%.3e]\n", 1/3);
+ printf("[%.3E]\n", 1/3);
+
+ // For `g` and `G` the precision specifies the number of significant
+ // digits to print before switching to exponential notation
+ printf("[%.3g]\n", 1000.1);
+ printf("[%.3G]\n", 1000.1);
+
+ // For strings, the precision specifies the amount of characters to
+ // print at most
+ printf("[%.5s]\n", "test");
+ printf("[%.3s]\n", "test");
+
+ // For JSON format, the precision specifies the amount of indentation
+ // to use. Omitting precision will not indent, specifying a precision
+ // of `0` uses tabs for indentation, any other precision uses this
+ // many spaces
+ printf("<%J>\n", [ 1, 2, 3, { true: false } ]), // no indent
+ printf("<%.J>\n", [ 1, 2, 3, { true: false } ]), // tab indent
+ printf("<%.0J>\n", [ 1, 2, 3, { true: false } ]), // tab indent
+ printf("<%.1J>\n", [ 1, 2, 3, { true: false } ]), // indent using one space
+ printf("<%.4J>\n", [ 1, 2, 3, { true: false } ]), // indent using four spaces
+
+ // precision does not apply to char, integer or `%` formats
+ printf("[%.3d]\n", 1000);
+ printf("[%.3c]\n", 65);
+ printf("[%.3%]\n");
+%}
+-- End --
+
+-- Expect stdout --
+[0.000]
+[0.000]
+[0.000e+00]
+[0.000E+00]
+[1e+03]
+[1E+03]
+[test]
+[tes]
+<[ 1, 2, 3, { "true": false } ]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+<[
+ 1,
+ 2,
+ 3,
+ {
+ "true": false
+ }
+]>
+[1000]
+[A]
+[%]
+-- End --
+
+
+A number of flag characters are supported for format directives.
+
+-- Testcase --
+{%
+ // The recognized flag characters are `#`, `0`, `-`, `+` and ` ` (space)
+ printf("%#0+- s\n", "test");
+
+ // Repetitions of flag characters are accepted
+ printf("%###s\n", "test");
+ printf("%000s\n", "test");
+ printf("%++-s\n", "test");
+ printf("%-- s\n", "test");
+ printf("% s\n", "test");
+
+ // The `#` flag produces alternative forms of various conversions
+ printf("%o / %#o\n", 15, 15);
+ printf("%x / %#x\n", 16, 16);
+ printf("%X / %#X\n", 17, 17);
+ printf("%g / %#g\n", 1.0, 1.0);
+
+ // The `0` flag indicates zero- instead of space-padding for various
+ // numeric conversions.
+ printf("[%5d / %05d]\n", -10, -10);
+ printf("[%5d / %05d]\n", 11, 11);
+ printf("[%5g / %05g]\n", -12.0, -12.0);
+ printf("[%5g / %05g]\n", 13.0, 13.0);
+ printf("[%5s / %05s]\n", "a", "a");
+
+ // The `-` flag indicates left, instead of right padding. It will
+ // override `0` and always pad with spaces
+ printf("[%-5d / %-05d]\n", -10, -10);
+ printf("[%-5d / %-05d]\n", 11, 11);
+ printf("[%-5g / %-05g]\n", -12.0, -12.0);
+ printf("[%-5g / %-05g]\n", 13.0, 13.0);
+ printf("[%-5s / %-05s]\n", "a", "a");
+
+ // The `+` flag indicates that a sign (`+` or `-`) should be placed
+ // before signed numeric values. It overrides ` ` (space).
+ printf("[%+5d / %+05d]\n", -10, -10);
+ printf("[%+5d / %+05d]\n", 11, 11);
+ printf("[%+5g / %+05g]\n", -12.0, -12.0);
+ printf("[%+5g / %+05g]\n", 13.0, 13.0);
+ printf("[%+5s / %+05s]\n", "a", "a");
+
+ // The ` ` (space) flag indicates that a blank should be placed
+ // before positive numbers (useful to ensure that negative and
+ // positive values in output are aligned)
+ printf("[%-5d / %- 5d]\n", -10, -10);
+ printf("[%-5d / %- 5d]\n", 11, 11);
+ printf("[%-5g / %- 5g]\n", -12.0, -12.0);
+ printf("[%-5g / %- 5g]\n", 13.0, 13.0);
+ printf("[%-5s / %- 5s]\n", "a", "a");
+%}
+-- End --
+
+-- Expect stdout --
+test
+test
+test
+test
+test
+test
+17 / 017
+10 / 0x10
+11 / 0X11
+1 / 1.00000
+[ -10 / -0010]
+[ 11 / 00011]
+[ -12 / -0012]
+[ 13 / 00013]
+[ a / a]
+[-10 / -10 ]
+[11 / 11 ]
+[-12 / -12 ]
+[13 / 13 ]
+[a / a ]
+[ -10 / -0010]
+[ +11 / +0011]
+[ -12 / -0012]
+[ +13 / +0013]
+[ a / a]
+[-10 / -10 ]
+[11 / 11 ]
+[-12 / -12 ]
+[13 / 13 ]
+[a / a ]
+-- End --
+
+
+Unrecognized format directives are copied to the output string as-is.
+
+-- Testcase --
+{%
+ // A truncated format directive is preserved
+ printf("test %\n", "test");
+ printf("test %-010.3\n", "test");
+
+ // An unrecognized format directive is preserved
+ printf("test %y test\n", 123);
+ printf("test %~123s test\n", 123);
+%}
+-- End --
+
+-- Expect stdout --
+test %
+test %-010.3
+test %y test
+test %~123s test
+-- End --
+
+
+Missing values for format directives are treated as `null`.
+
+-- Testcase --
+{%
+ printf("%s\n");
+ printf("%d\n");
+ printf("%u\n");
+ printf("%o\n");
+ printf("%x\n");
+ printf("%X\n");
+ printf("%f\n");
+ printf("%F\n");
+ printf("%e\n");
+ printf("%E\n");
+ printf("%g\n");
+ printf("%G\n");
+ //printf("%c\n");
+ printf("%J\n");
+%}
+-- End --
+
+-- Expect stdout --
+(null)
+0
+0
+0
+0
+0
+0.000000
+0.000000
+0.000000e+00
+0.000000E+00
+0
+0
+null
+-- End --
+
+
+Supplying a non-string format value will yield an empty string result.
+
+-- Testcase --
+{%
+ printf(true, 1, 2, 3);
+%}
+-- End --
+
+-- Expect stdout --
+-- End --
diff --git a/tests/custom/03_stdlib/29_require b/tests/custom/03_stdlib/29_require
new file mode 100644
index 0000000..681f3f7
--- /dev/null
+++ b/tests/custom/03_stdlib/29_require
@@ -0,0 +1,164 @@
+The `require()` function loads the specified module, executes it and returns
+the returned value to the caller.
+
+The global array `REQUIRE_SEARCH_PATH` specifies the list of locations to
+check for a matching module file.
+
+The return value of a successfully loaded module is cached in a global
+registry, subsequent require calls with the same name will return the
+cached value.
+
+Throws an exception if the global `REQUIRE_SEARCH_PATH` variable is unset or
+not pointing to an array.
+
+Throws an exception if the requested module name cannot be found.
+
+Throws an exception if a module file could be found but not opened.
+
+Throws an exception if a module file could not be compiled.
+
+Returns the value returned by the invoked module code (typically an object).
+
+-- Testcase --
+{%
+ push(REQUIRE_SEARCH_PATH, TESTFILES_PATH + '/*.uc');
+
+ let mod1 = require("require.test.module");
+ printf("require() #1 returned %.J\n\n", mod1);
+
+ let mod2 = require("require.test.module");
+ printf("require() #2 returned %.J\n\n", mod2);
+
+ printf("Instances are identical: %s\n\n", mod1 === mod2);
+
+ // deleting the entry from the global module registry forces reload
+ delete global.modules["require.test.module"];
+
+ let mod3 = require("require.test.module");
+ printf("require() #3 returned %.J\n\n", mod3);
+
+ printf("Instances are identical: %s\n\n", mod1 === mod3);
+%}
+-- End --
+
+-- File require/test/module.uc --
+{%
+ print("This is require.test.module running!\n\n");
+
+ return {
+ greeting: function(name) {
+ printf("Hello, %s!\n", name);
+ }
+ };
+%}
+-- End --
+
+-- Expect stdout --
+This is require.test.module running!
+
+require() #1 returned {
+ "greeting": "function(name) { ... }"
+}
+
+require() #2 returned {
+ "greeting": "function(name) { ... }"
+}
+
+Instances are identical: true
+
+This is require.test.module running!
+
+require() #3 returned {
+ "greeting": "function(name) { ... }"
+}
+
+Instances are identical: false
+
+-- End --
+
+
+A clobbered `REQUIRE_SEARCH_PATH` triggers an exception.
+
+-- Testcase --
+{%
+ REQUIRE_SEARCH_PATH = null;
+
+ require("test");
+%}
+-- End --
+
+-- Expect stderr --
+Runtime error: Global require search path not set
+In line 4, byte 16:
+
+ ` require("test");`
+ Near here --------^
+
+
+-- End --
+
+
+A not found module triggers an exception.
+
+-- Testcase --
+{%
+ require("test");
+%}
+-- End --
+
+-- Expect stderr --
+Runtime error: No module named 'test' could be found
+In line 2, byte 16:
+
+ ` require("test");`
+ Near here --------^
+
+
+-- End --
+
+
+A compilation error in the module triggers an exception.
+
+-- Testcase --
+{%
+ try {
+ push(REQUIRE_SEARCH_PATH, TESTFILES_PATH + '/*.uc');
+
+ require("require.test.broken");
+ }
+ catch (e) {
+ // Catch and rethrow exception with modified message to
+ // ensure stable test output.
+ e.message = replace(e.message,
+ /(compile module '.+require\/test\/broken\.uc')/,
+ "compile module '.../require/test/broken.uc'");
+
+ die(e);
+ }
+%}
+-- End --
+
+-- File require/test/broken.uc --
+{%
+ // Unclosed object to force syntax error
+ return {
+%}
+-- End --
+
+-- Expect stderr --
+Unable to compile module '.../require/test/broken.uc':
+Syntax error: Expecting label
+In line 3, byte 11:
+
+ ` return {`
+ Near here --^
+
+
+
+In line 14, byte 8:
+
+ ` die(e);`
+ Near here ---^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/30_iptoarr b/tests/custom/03_stdlib/30_iptoarr
new file mode 100644
index 0000000..5f1ae24
--- /dev/null
+++ b/tests/custom/03_stdlib/30_iptoarr
@@ -0,0 +1,46 @@
+The `iptoarr()` function parses the given IP address string into an array
+of byte values.
+
+Returns an array of byte values for the parsed IP address.
+
+Returns `null` if the given IP argument is not a string value or if the
+IP address could not be parsed.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ iptoarr("0.0.0.0"),
+ iptoarr("10.11.12.13"),
+ iptoarr("::"),
+ iptoarr("::ffff:192.168.1.1"),
+ iptoarr("2001:db8:1234:4567:789a:bcde:f012:3456"),
+ iptoarr("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ 0, 0, 0, 0 ]
+[ 10, 11, 12, 13 ]
+[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 192, 168, 1, 1 ]
+[ 32, 1, 13, 184, 18, 52, 69, 103, 120, 154, 188, 222, 240, 18, 52, 86 ]
+[ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ]
+-- End --
+
+
+Supplying a non-string value or an unparsable address yields `null`.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ iptoarr(true),
+ iptoarr("invalid")
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+null
+null
+-- End --
diff --git a/tests/custom/03_stdlib/31_arrtoip b/tests/custom/03_stdlib/31_arrtoip
new file mode 100644
index 0000000..61d82e1
--- /dev/null
+++ b/tests/custom/03_stdlib/31_arrtoip
@@ -0,0 +1,57 @@
+The `arrtoip()` function converts the given byte array into an IP address
+string. Array of length 4 are converted to IPv4 addresses, arrays of
+length 16 to IPv6 addresses.
+
+Returns the resulting IPv4 or IPv6 address string.
+
+Returns `null` if the given value is not an array, if the array has an
+unsuitable length or if any item within the array is not an integer within
+the range 0-255.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ arrtoip([ 0, 0, 0, 0 ]),
+ arrtoip([ 192, 168, 1, 1 ]),
+ arrtoip([ 255, 255, 255, 255 ]),
+ arrtoip([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]),
+ arrtoip([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 192, 168, 1, 1 ]),
+ arrtoip([ 32, 1, 13, 184, 18, 52, 69, 103, 120, 154, 188, 222, 240, 18, 52, 86 ]),
+ arrtoip([ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ])
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+0.0.0.0
+192.168.1.1
+255.255.255.255
+::
+::ffff:192.168.1.1
+2001:db8:1234:4567:789a:bcde:f012:3456
+ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
+-- End --
+
+
+Supplying a non-array value, an array of unsuitable length or an array
+containing invalid byte values yields `null`.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ arrtoip(true),
+ arrtoip([ ]),
+ arrtoip([ 1, 2, 3 ]),
+ arrtoip([ 192, 168, 1, -1 ]),
+ arrtoip([ true, false, -5, 500 ])
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+null
+null
+null
+null
+null
+-- End --
diff --git a/tests/custom/03_stdlib/32_match b/tests/custom/03_stdlib/32_match
new file mode 100644
index 0000000..b053044
--- /dev/null
+++ b/tests/custom/03_stdlib/32_match
@@ -0,0 +1,55 @@
+The `match()` function applies the given regular expression pattern on
+the given subject value.
+
+Depending on whether the given regular expression sets the global (`g`)
+modifier, either an array of match groups or the first match group is
+returned.
+
+Returns `null` if the given pattern argument is not a regular expression
+value, or if the subject is `null` or unspecified.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ // match all key=value pairs
+ match("kind=fruit name=strawberry color=red", /([[:alpha:]]+)=([^= ]+)/g),
+
+ // match any word
+ match("The quick brown fox jumps over the lazy dog", /[[:alpha:]]+/g),
+
+ // match the first three lowercase words
+ match("The quick brown fox jumps over the lazy dog", / ([[:lower:]]+) ([[:lower:]]+) ([[:lower:]]+)/),
+
+ // special case: match any empty string sequence
+ match("foo", /()/g),
+
+ // special case: match first empty string sequence
+ match("foo", /()/),
+
+ // subject is implictly converted to string
+ match(true, /u/)
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+[ [ "kind=fruit", "kind", "fruit" ], [ "name=strawberry", "name", "strawberry" ], [ "color=red", "color", "red" ] ]
+[ [ "The" ], [ "quick" ], [ "brown" ], [ "fox" ], [ "jumps" ], [ "over" ], [ "the" ], [ "lazy" ], [ "dog" ] ]
+[ " quick brown fox", "quick", "brown", "fox" ]
+[ [ "", "" ], [ "", "" ], [ "", "" ], [ "", "" ] ]
+[ "", "" ]
+[ "u" ]
+-- End --
+
+
+Omitting the subject yields `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", match(null, /u/));
+%}
+-- End --
+
+-- Expect stdout --
+null
+-- End --
diff --git a/tests/custom/03_stdlib/33_replace b/tests/custom/03_stdlib/33_replace
new file mode 100644
index 0000000..b662ae8
--- /dev/null
+++ b/tests/custom/03_stdlib/33_replace
@@ -0,0 +1,207 @@
+The `replace()` function replaces the given regular expression or plain
+string pattern on the given subject value with the specified replacement.
+
+In case a regular expression with the global (`g`) modifier set or a
+string is passed as pattern, all found occurrences are replaced. In case
+a regular expression without global modifier is given, only the first
+match will be replaced.
+
+The replacement value may be either a string, which is inserted in place
+of the matched result after certain interpolation steps or a function
+which is invoked for each match and whose return value is used as
+replacement.
+
+The subject is implicitly converted to a string if it is not a string.
+
+The pattern is implicitly converted to a string if it is neither a string
+nor a regular expression value.
+
+The replacement value is implicitly converted to a string if it is neither
+a string nor a function value.
+
+Returns a copy of the input string with the match(es) replaced by their
+corresponding replacement values.
+
+Returns `null` either the subject, the pattern or the replacement value
+is `null`.
+
+-- Testcase --
+{%
+ print(join("\n###\n", [
+ // Capitalize and reformat all key=value pairs using a callback
+ replace("kind=fruit name=strawberry color=red",
+ /([[:alpha:]])([[:alpha:]]*)=(.)([^= ]*) */g,
+ function(m, letter1, rest1, letter2, rest2) {
+ return sprintf('%s%s: %s%s\n',
+ uc(letter1), rest1,
+ uc(letter2), rest2
+ );
+ }),
+
+ // strike any three letter word
+ replace("The quick brown fox jumps over the lazy dog",
+ /(^| )([[:alpha:]]{3})( |$)/g,
+ "$1<s>$2</s>$3"),
+
+ // highlight any vowel
+ replace("The quick brown fox jumps over the lazy dog",
+ /[aeiou]/g,
+ "[$&]"),
+
+ // replace with fixed pattern
+ replace("foo bar foo baz foo qrx", "foo", "xxx"),
+
+ // testing all possible replacement interpolations
+ replace("before abc def ghi jkl mno pqr stu vwx yz! after",
+ / ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z!]{3}) /,
+ '|\n---\n' +
+ 'Entire match ($$&): [$&]\n' +
+ 'Before match ($$`): [$`]\n' +
+ "After match ($$'): [$']\n" +
+ 'Group 1 match ($$1): [$1]\n' +
+ 'Group 2 match ($$2): [$2]\n' +
+ 'Group 3 match ($$3): [$3]\n' +
+ 'Group 4 match ($$4): [$4]\n' +
+ 'Group 5 match ($$5): [$5]\n' +
+ 'Group 6 match ($$6): [$6]\n' +
+ 'Group 7 match ($$7): [$7]\n' +
+ 'Group 8 match ($$8): [$8]\n' +
+ 'Group 9 match ($$9): [$9]\n' +
+ 'Literal $$: [$$]\n' +
+ '---\n|'),
+
+ // testing that all captures are passed to the callback
+ replace("before abc def ghi jkl mno pqr stu vwx yz! after",
+ / ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z]{3}) ([a-z!]{3}) /,
+ function(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9) {
+ return sprintf(
+ '|\n---\n' +
+ 'Entire match (arg 0): [%s]\n' +
+ 'Group 1 match (arg 1): [%s]\n' +
+ 'Group 2 match (arg 2): [%s]\n' +
+ 'Group 3 match (arg 3): [%s]\n' +
+ 'Group 4 match (arg 4): [%s]\n' +
+ 'Group 5 match (arg 5): [%s]\n' +
+ 'Group 6 match (arg 6): [%s]\n' +
+ 'Group 7 match (arg 7): [%s]\n' +
+ 'Group 8 match (arg 8): [%s]\n' +
+ 'Group 9 match (arg 9): [%s]\n' +
+ '---\n|',
+ m0, m1, m2, m3, m4, m5, m6, m7, m8, m9
+ );
+ }),
+
+ // the subject is implictly stringified
+ replace({ foo: true }, "foo", "xxx"),
+
+ // the pattern is implictly stringified
+ replace({ foo: true }, true, "false"),
+
+ // the replacement is implictly stringified
+ replace({ foo: true }, "foo", 0x7b),
+
+ // special case: replace all empty matches
+ replace("foo", "", "."),
+ replace("foo", /()/g, ".")
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+Kind: Fruit
+Name: Strawberry
+Color: Red
+
+###
+<s>The</s> quick brown <s>fox</s> jumps over <s>the</s> lazy <s>dog</s>
+###
+Th[e] q[u][i]ck br[o]wn f[o]x j[u]mps [o]v[e]r th[e] l[a]zy d[o]g
+###
+xxx bar xxx baz xxx qrx
+###
+before |
+---
+Entire match ($&): [ abc def ghi jkl mno pqr stu vwx yz! ]
+Before match ($`): [before ]
+After match ($'): [ after]
+Group 1 match ($1): [abc]
+Group 2 match ($2): [def]
+Group 3 match ($3): [ghi]
+Group 4 match ($4): [jkl]
+Group 5 match ($5): [mno]
+Group 6 match ($6): [pqr]
+Group 7 match ($7): [stu]
+Group 8 match ($8): [vwx]
+Group 9 match ($9): [yz!]
+Literal $: [$]
+---
+| after
+###
+before |
+---
+Entire match (arg 0): [ abc def ghi jkl mno pqr stu vwx yz! ]
+Group 1 match (arg 1): [abc]
+Group 2 match (arg 2): [def]
+Group 3 match (arg 3): [ghi]
+Group 4 match (arg 4): [jkl]
+Group 5 match (arg 5): [mno]
+Group 6 match (arg 6): [pqr]
+Group 7 match (arg 7): [stu]
+Group 8 match (arg 8): [vwx]
+Group 9 match (arg 9): [yz!]
+---
+| after
+###
+{ "xxx": true }
+###
+{ "foo": false }
+###
+{ "123": true }
+###
+.f.o.o.
+###
+.f.o.o.
+-- End --
+
+
+Omitting subject, pattern or replacement yields `null`.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ replace(null, "u", "x"),
+ replace("nullnull", null, "x"),
+ replace("foo", "o", null)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ null,
+ null,
+ null
+]
+-- End --
+
+
+Exceptions in the callback terminate the replacement process and are
+propagated to the calling context.
+
+-- Testcase --
+{%
+ replace("foo", "o", function(m) { die() });
+%}
+-- End --
+
+-- Expect stderr --
+Died
+In [anonymous function](), line 2, byte 40:
+ called from function replace ([C])
+ called from anonymous function ([stdin]:2:43)
+
+ ` replace("foo", "o", function(m) { die() });`
+ Near here --------------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/34_json b/tests/custom/03_stdlib/34_json
new file mode 100644
index 0000000..ba8ad9f
--- /dev/null
+++ b/tests/custom/03_stdlib/34_json
@@ -0,0 +1,110 @@
+The `json()` function parses the given string value as JSON.
+
+Throws an exception if the given input value is not a string.
+Throws an exception if the given input string cannot be parsed as JSON.
+
+Returns the resulting value.
+
+-- Testcase --
+{%
+ print(join("\n", [
+ json("null"),
+ json("true"),
+ json("false"),
+ json("123"),
+ json("456.7890000"),
+ json("-1.4E10"),
+ json("1e309"),
+ json('"A string \u2600"'),
+ json("[ 1, 2, 3 ]"),
+ json('{ "test": [ 1, 2, 3 ] }'),
+
+ // surrounding white space is ignored
+ json(' [ 1, 2, 3 ] ')
+ ]), "\n");
+%}
+-- End --
+
+-- Expect stdout --
+null
+true
+false
+123
+456.789
+-1.4e+10
+Infinity
+A string ☀
+[ 1, 2, 3 ]
+{ "test": [ 1, 2, 3 ] }
+[ 1, 2, 3 ]
+-- End --
+
+
+Passing a non-string value throws an exception.
+
+-- Testcase --
+{%
+ json(true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed value is not a string
+In line 2, byte 11:
+
+ ` json(true);`
+ Near here ---^
+
+
+-- End --
+
+
+Unparseable JSON throws exceptions.
+
+-- Testcase --
+{%
+ json('[ "incomplete", "array" ');
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Failed to parse JSON string: unexpected end of data
+In line 2, byte 33:
+
+ ` json('[ "incomplete", "array" ');`
+ Near here -------------------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ json('invalid syntax');
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Failed to parse JSON string: unexpected character
+In line 2, byte 23:
+
+ ` json('invalid syntax');`
+ Near here ---------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ json('[] trailing garbage');
+%}
+-- End --
+
+-- Expect stderr --
+Syntax error: Trailing garbage after JSON data
+In line 2, byte 28:
+
+ ` json('[] trailing garbage');`
+ Near here --------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/35_include b/tests/custom/03_stdlib/35_include
new file mode 100644
index 0000000..6d808f2
--- /dev/null
+++ b/tests/custom/03_stdlib/35_include
@@ -0,0 +1,173 @@
+The `include()` function executes the specified path as ucode script,
+optionally setting a different execution scope for the invoked file.
+
+If the specified path is relative, it is treated as being relative to the
+source file currently being executed or the current working directory in
+case the interpreter executes code from stdin or a command line argument.
+
+Throws an exception if the given path value is not a string.
+
+Throws an exception if a scope argument is specified and not a valid object.
+
+Throws an exception if the given path could not be found or opened.
+
+Throws an exception if the given file could not be compiled.
+
+Returns no value.
+
+-- Testcase --
+{%
+ let real_printf = printf;
+
+ // include by relative path
+ include("files/include.uc");
+
+ printf("---\n");
+
+ // include by absolute path
+ include(TESTFILES_PATH + "/include.uc");
+
+ printf("---\n");
+
+ // include with overridden scope
+ include("files/include.uc", {
+ printf: function(...args) {
+ real_printf("This is the wrapped printf() getting called!\n");
+
+ return real_printf(...args);
+ }
+ });
+
+ printf("---\n");
+
+ // include with isolated scope
+ include("files/include.uc", proto({
+ printf: function(...args) {
+ real_printf("This is the wrapped printf() getting called!\n");
+
+ return real_printf(...args);
+ }
+ }, {}));
+%}
+-- End --
+
+-- File include.uc --
+{%
+ printf("This is the include file running! Can I access the global env? %s\n",
+ REQUIRE_SEARCH_PATH ? "Yes!" : "No.");
+%}
+-- End --
+
+-- Expect stdout --
+This is the include file running! Can I access the global env? Yes!
+---
+This is the include file running! Can I access the global env? Yes!
+---
+This is the wrapped printf() getting called!
+This is the include file running! Can I access the global env? Yes!
+---
+This is the wrapped printf() getting called!
+This is the include file running! Can I access the global env? No.
+-- End --
+
+
+An invalid path value triggers an exception.
+
+-- Testcase --
+{%
+ include(true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed filename is not a string
+In line 2, byte 14:
+
+ ` include(true);`
+ Near here ------^
+
+
+-- End --
+
+
+An invalid scope value triggers an exception.
+
+-- Testcase --
+{%
+ include("test", true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed scope value is not an object
+In line 2, byte 22:
+
+ ` include("test", true);`
+ Near here --------------^
+
+
+-- End --
+
+
+A not found file triggers an exception.
+
+-- Testcase --
+{%
+ include("files/doesnotexist.uc");
+%}
+-- End --
+
+-- Expect stderr --
+Runtime error: Include file not found
+In line 2, byte 33:
+
+ ` include("files/doesnotexist.uc");`
+ Near here -------------------------^
+
+
+-- End --
+
+
+A compilation error in the file triggers an exception.
+
+-- Testcase --
+{%
+ try {
+ include("files/broken.uc");
+ }
+ catch (e) {
+ // Catch and rethrow exception with modified message to
+ // ensure stable test output.
+ e.message = replace(e.message,
+ /(compile module '.+broken\.uc')/,
+ "compile module '.../broken.uc'");
+
+ die(e);
+ }
+%}
+-- End --
+
+-- File broken.uc --
+{%
+ // Unclosed object to force syntax error
+ return {
+%}
+-- End --
+
+-- Expect stderr --
+Unable to compile module '.../broken.uc':
+Syntax error: Expecting label
+In line 3, byte 11:
+
+ ` return {`
+ Near here --^
+
+
+
+In line 12, byte 8:
+
+ ` die(e);`
+ Near here ---^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/36_render b/tests/custom/03_stdlib/36_render
new file mode 100644
index 0000000..64ef08a
--- /dev/null
+++ b/tests/custom/03_stdlib/36_render
@@ -0,0 +1,167 @@
+The `render()` function executes the specified path as ucode script,
+optionally setting a different execution scope for the invoked file,
+and captures the produced output in a string.
+
+If the specified path is relative, it is treated as being relative to the
+source file currently being executed or the current working directory in
+case the interpreter executes code from stdin or a command line argument.
+
+Throws an exception if the given path value is not a string.
+
+Throws an exception if a scope argument is specified and not a valid object.
+
+Throws an exception if the given path could not be found or opened.
+
+Throws an exception if the given file could not be compiled.
+
+Returns a string containing the captured output of the executed file.
+
+-- Testcase --
+{%
+ let real_printf = printf;
+
+ printf("%.J\n", [
+ // include by relative path
+ render("files/include.uc"),
+
+ // include by absolute path
+ render(TESTFILES_PATH + "/include.uc"),
+
+ // include with overridden scope
+ render("files/include.uc", {
+ printf: function(...args) {
+ real_printf("This is the wrapped printf() getting called!\n");
+
+ return real_printf(...args);
+ }
+ }),
+
+ // include with isolated scope
+ render("files/include.uc", proto({
+ printf: function(...args) {
+ real_printf("This is the wrapped printf() getting called!\n");
+
+ return real_printf(...args);
+ }
+ }, {}))
+ ]);
+%}
+-- End --
+
+-- File include.uc --
+{%
+ printf("This is the include file running! Can I access the global env? %s\n",
+ REQUIRE_SEARCH_PATH ? "Yes!" : "No.");
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "This is the include file running! Can I access the global env? Yes!\n",
+ "This is the include file running! Can I access the global env? Yes!\n",
+ "This is the wrapped printf() getting called!\nThis is the include file running! Can I access the global env? Yes!\n",
+ "This is the wrapped printf() getting called!\nThis is the include file running! Can I access the global env? No.\n"
+]
+-- End --
+
+
+An invalid path value triggers an exception.
+
+-- Testcase --
+{%
+ include(true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed filename is not a string
+In line 2, byte 14:
+
+ ` include(true);`
+ Near here ------^
+
+
+-- End --
+
+
+An invalid scope value triggers an exception.
+
+-- Testcase --
+{%
+ include("test", true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed scope value is not an object
+In line 2, byte 22:
+
+ ` include("test", true);`
+ Near here --------------^
+
+
+-- End --
+
+
+A not found file triggers an exception.
+
+-- Testcase --
+{%
+ include("files/doesnotexist.uc");
+%}
+-- End --
+
+-- Expect stderr --
+Runtime error: Include file not found
+In line 2, byte 33:
+
+ ` include("files/doesnotexist.uc");`
+ Near here -------------------------^
+
+
+-- End --
+
+
+A compilation error in the file triggers an exception.
+
+-- Testcase --
+{%
+ try {
+ include("files/broken.uc");
+ }
+ catch (e) {
+ // Catch and rethrow exception with modified message to
+ // ensure stable test output.
+ e.message = replace(e.message,
+ /(compile module '.+broken\.uc')/,
+ "compile module '.../broken.uc'");
+
+ die(e);
+ }
+%}
+-- End --
+
+-- File broken.uc --
+{%
+ // Unclosed object to force syntax error
+ return {
+%}
+-- End --
+
+-- Expect stderr --
+Unable to compile module '.../broken.uc':
+Syntax error: Expecting label
+In line 3, byte 11:
+
+ ` return {`
+ Near here --^
+
+
+
+In line 12, byte 8:
+
+ ` die(e);`
+ Near here ---^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/37_warn b/tests/custom/03_stdlib/37_warn
new file mode 100644
index 0000000..2c0ff24
--- /dev/null
+++ b/tests/custom/03_stdlib/37_warn
@@ -0,0 +1,40 @@
+The `warn()` function outputs the given values to stderr.
+
+Returns the amount of bytes written.
+
+-- Testcase --
+{%
+ let n = 0;
+
+ n += warn(null, "\n");
+ n += warn(true, "\n");
+ n += warn(false, "\n");
+ n += warn(123, "\n");
+ n += warn(456.789, "\n");
+ n += warn(NaN, "\n");
+ n += warn(Infinity, "\n");
+ n += warn("Hello world", "\n");
+ n += warn([ 1, 2, 3 ], "\n");
+ n += warn({ some: "dict" }, "\n");
+ n += warn(warn, "\n");
+
+ warn(n, " bytes written\n");
+%}
+-- End --
+
+-- Expect stderr --
+
+true
+false
+123
+456.789
+NaN
+Infinity
+Hello world
+[ 1, 2, 3 ]
+{ "some": "dict" }
+function warn(...) { [native code] }
+117 bytes written
+-- End --
+
+
diff --git a/tests/custom/03_stdlib/38_system b/tests/custom/03_stdlib/38_system
new file mode 100644
index 0000000..faf8aa5
--- /dev/null
+++ b/tests/custom/03_stdlib/38_system
@@ -0,0 +1,148 @@
+The `system()` function executes the given shell command or raw command
+vector, optionally terminating the spawned process after the specified
+timeout.
+
+Throws an exception if a timeout is specified but not a valid positive
+integer value.
+
+Throws an exception if the command argument is neither an array nor a
+string value.
+
+Throws an exception if an empty command vector is given.
+
+Returns the exit code of the invoked process.
+
+-- Testcase --
+{%
+ // When passing the command as string, `/bin/sh -c` is invoked with
+ // the given command string as second argument
+ system('x=1; echo $((x + x))');
+
+ // When passing the command as array, the first value is taken as
+ // executable to invoke and any further item as argument to the
+ // invoked program. Internally `execvp()` is used, which means that
+ // the executable path may be relative in which case it is looked
+ // up in the directories specified by `$PATH`. Any array items are
+ // implicitly stringified.
+ system([ '/bin/sh', TESTFILES_PATH + '/testscripts/hello.sh', true, 0x42, 123.456000, { some: "dict" } ]);
+
+ // By specifying a timeout, maximum execution time is limited to
+ // that many milliseconds. If the program does not finish before the
+ // timeout occurs, it is forcibly terminated with SIGKILL.
+ system([ '/bin/sh', TESTFILES_PATH + '/testscripts/sleep.sh' ], 100);
+
+ // The return value of system() is the exit code of the invoked program.
+ let rc = system([ '/bin/sh', TESTFILES_PATH + '/testscripts/exit.sh' ]);
+
+ printf("Return value is %d\n", rc);
+%}
+-- End --
+
+-- File testscripts/hello.sh --
+#!/bin/sh
+
+echo "This is our test program running!"
+echo "My arguments are:"
+
+for arg in "$@"; do
+ echo "<$arg>"
+done
+-- End --
+
+-- File testscripts/sleep.sh --
+#!/bin/sh
+
+echo "I'll sleep for 10s now..."
+sleep 10
+echo "I am done sleeping."
+-- End --
+
+-- File testscripts/exit.sh --
+#!/bin/sh
+
+echo "I'll exit with code 5 now."
+exit 5
+-- End --
+
+-- Expect stdout --
+2
+This is our test program running!
+My arguments are:
+<true>
+<66>
+<123.456>
+<{ "some": "dict" }>
+I'll sleep for 10s now...
+I'll exit with code 5 now.
+Return value is 5
+-- End --
+
+
+Passing an invalid command value throws an exception.
+
+-- Testcase --
+{%
+ system(true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed command is neither string nor array
+In line 2, byte 13:
+
+ ` system(true);`
+ Near here -----^
+
+
+-- End --
+
+-- Testcase --
+{%
+ system([]);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed command array is empty
+In line 2, byte 11:
+
+ ` system([]);`
+ Near here ---^
+
+
+-- End --
+
+
+Passing an invalid timeout throws an exception.
+
+-- Testcase --
+{%
+ system("exit 0", "invalid")
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Invalid timeout specified
+In line 2, byte 28:
+
+ ` system("exit 0", "invalid")`
+ Near here --------------------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ system("exit 0", -100)
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Invalid timeout specified
+In line 2, byte 23:
+
+ ` system("exit 0", -100)`
+ Near here ---------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/39_trace b/tests/custom/03_stdlib/39_trace
new file mode 100644
index 0000000..2b2295a
--- /dev/null
+++ b/tests/custom/03_stdlib/39_trace
@@ -0,0 +1,79 @@
+The `trace()` function sets the execution trace level of the VM.
+
+Throws an exception if trace level argument is not a valid integer.
+
+Returns the previously used execution trace level.
+
+-- Testcase --
+{%
+ printf("Code before enabling tracing.\n");
+
+ trace(1);
+
+ printf("Code after enabling tracing.\n");
+
+ trace(0);
+
+ printf("Code after disabling tracing.\n");
+%}
+-- End --
+
+-- Expect stdout --
+Code before enabling tracing.
+Code after enabling tracing.
+Code after disabling tracing.
+-- End --
+
+-- Expect stderr --
+ [-2] 1
+ [-1] "function trace(...) { [native code] }"
+ [+1] 0
+ [stdin]:4 trace(1);
+0000001c POP
+ [-1] 0
+0000001d LVAR {0x0} ; "printf"
+ [+1] "function printf(...) { [native code] }"
+ [stdin]:6 printf("Code after enabling tracing.\n");
+00000022 LOAD {0x3} ; "Code after enabling tracing.\n"
+ [+2] "Code after enabling tracing.\n"
+ [stdin]:6 printf("Code after enabling tracing.\n");
+00000027 CALL {0x1}
+ [*] CALLFRAME[1]
+ |- stackframe 1/3
+ |- ctx null
+ [-2] "Code after enabling tracing.\n"
+ [-1] "function printf(...) { [native code] }"
+ [+1] 29
+ [stdin]:6 printf("Code after enabling tracing.\n");
+0000002c POP
+ [-1] 29
+0000002d LVAR {0x2} ; "trace"
+ [+1] "function trace(...) { [native code] }"
+ [stdin]:8 trace(0);
+00000032 LOAD8 {0}
+ [+2] 0
+ [stdin]:8 trace(0);
+00000034 CALL {0x1}
+ [*] CALLFRAME[1]
+ |- stackframe 1/3
+ |- ctx null
+-- End --
+
+
+Passing an invalid trace value throws an exception.
+
+-- Testcase --
+{%
+ trace("inval");
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Invalid level specified
+In line 2, byte 15:
+
+ ` trace("inval");`
+ Near here -------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/40_proto b/tests/custom/03_stdlib/40_proto
new file mode 100644
index 0000000..017a821
--- /dev/null
+++ b/tests/custom/03_stdlib/40_proto
@@ -0,0 +1,89 @@
+The `proto()` function retrievs or sets the prototype of the given object
+or resource value.
+
+Throws an exception if given value does not support setting prototypes.
+
+When invoked with one argument, returns the prototype of the given value
+(if any).
+
+When invoked with two arguments, returns the given value.
+
+-- Testcase --
+{%
+ let fs = require("fs");
+
+ // create a "class instance" by attaching a function dictionary to
+ // a plain object.
+ let obj = proto({}, {
+ greeting: function(name) {
+ printf("Hello, %s!\n", name);
+ }
+ });
+
+ // accessing a property on `obj` will look up the prototype chain
+ // if the object itself does not have it
+ obj.greeting("World");
+
+ printf("%.J\n", [
+ // retrieve prototype of `fs.file` resource
+ proto(fs.stdout),
+
+ // retrieve prototype of `obj`
+ proto(obj)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+Hello, World!
+[
+ {
+ "error": "function error(...) { [native code] }",
+ "fileno": "function fileno(...) { [native code] }",
+ "close": "function close(...) { [native code] }",
+ "tell": "function tell(...) { [native code] }",
+ "seek": "function seek(...) { [native code] }",
+ "write": "function write(...) { [native code] }",
+ "read": "function read(...) { [native code] }"
+ },
+ {
+ "greeting": "function(name) { ... }"
+ }
+]
+-- End --
+
+
+
+Passing an invalid value throws an exception.
+
+-- Testcase --
+{%
+ proto("inval", {});
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed value is neither a prototype, resource or object
+In line 2, byte 19:
+
+ ` proto("inval", {});`
+ Near here -----------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ proto({}, "inval");
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Passed value is neither a prototype, resource or object
+In line 2, byte 19:
+
+ ` proto({}, "inval");`
+ Near here -----------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/41_sleep b/tests/custom/03_stdlib/41_sleep
new file mode 100644
index 0000000..bca6f0d
--- /dev/null
+++ b/tests/custom/03_stdlib/41_sleep
@@ -0,0 +1,46 @@
+The `sleep()` function pauses program execution for the given amount of
+milliseconds.
+
+Returns `true` if the program slept.
+
+Returns `false` when the given time value was not convertible to an integer,
+negative or zero.
+
+-- Testcase --
+{%
+ let t1 = time();
+
+ sleep(1000);
+
+ let t2 = time();
+
+ printf("Slept for %d second(s).\n", t2 - t1);
+%}
+-- End --
+
+-- Expect stdout --
+Slept for 1 second(s).
+-- End --
+
+
+Passing an invalid value yields `false`.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ sleep("inval"),
+ sleep([]),
+ sleep(-1),
+ sleep(0)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ false,
+ false,
+ false,
+ false
+]
+-- End --
diff --git a/tests/custom/03_stdlib/42_assert b/tests/custom/03_stdlib/42_assert
new file mode 100644
index 0000000..e6200e9
--- /dev/null
+++ b/tests/custom/03_stdlib/42_assert
@@ -0,0 +1,30 @@
+The `assert()` function raises an exception using the second argument as
+message when the first argument value is not truish.
+
+Throws an exception if the first argument value is not truish.
+
+Returns the value of the first argument.
+
+-- Testcase --
+{%
+ let x = assert(123, "This should not trigger");
+ printf("x = %d\n", x);
+
+ let y = assert(false, "This should trigger");
+ printf("y = %d\n", y);
+%}
+-- End --
+
+-- Expect stdout --
+x = 123
+-- End --
+
+-- Expect stderr --
+This should trigger
+In line 5, byte 45:
+
+ ` let y = assert(false, "This should trigger");`
+ Near here -------------------------------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/43_regexp b/tests/custom/03_stdlib/43_regexp
new file mode 100644
index 0000000..9809c87
--- /dev/null
+++ b/tests/custom/03_stdlib/43_regexp
@@ -0,0 +1,117 @@
+The `regexp()` function compiles the given pattern string into a regular
+expression, optionally applying the flags specified in the second argument.
+
+Throws an exception if unrecognized flag characters are specified or if the
+flags argument is not a string value.
+
+Throws an exception if the given pattern string cannot be compiled into a
+regular expression.
+
+Returns the compiled regexp object.
+
+-- Testcase --
+{%
+ let re1 = regexp("begin (.+) end", "i");
+ let re2 = regexp("[a-z]+", "g");
+ let re3 = regexp("Dots (.+) newlines", "s");
+
+ printf("%.J\n", [
+ match("BEGIN this is some text END", re1),
+ match("This is a group of words", re2),
+ match("Dots now\ndon't\nmatch\ntext\nwith newlines", re3)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ [
+ "BEGIN this is some text END",
+ "this is some text"
+ ],
+ [
+ [
+ "his"
+ ],
+ [
+ "is"
+ ],
+ [
+ "a"
+ ],
+ [
+ "group"
+ ],
+ [
+ "of"
+ ],
+ [
+ "words"
+ ]
+ ],
+ null
+]
+-- End --
+
+
+Passing an uncompilable regexp throws an exception.
+
+-- Testcase --
+{%
+ try {
+ // unterminated capture group to trigger syntax error
+ regexp("foo(");
+ }
+ catch (e) {
+ // Massage compile error message for stable output since it is
+ // dependant on the underyling C library.
+ e.message = "Compile error";
+ die(e);
+ }
+%}
+-- End --
+
+-- Expect stderr --
+Compile error
+In line 10, byte 8:
+
+ ` die(e);`
+ Near here ---^
+
+
+-- End --
+
+
+Passing an invalid flags argument throws an exception.
+
+-- Testcase --
+{%
+ regexp(".*", true);
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Given flags argument is not a string
+In line 2, byte 19:
+
+ ` regexp(".*", true);`
+ Near here -----------^
+
+
+-- End --
+
+-- Testcase --
+{%
+ regexp(".*", "igz");
+%}
+-- End --
+
+-- Expect stderr --
+Type error: Unrecognized flag character 'z'
+In line 2, byte 20:
+
+ ` regexp(".*", "igz");`
+ Near here ------------^
+
+
+-- End --
diff --git a/tests/custom/03_stdlib/44_wildcard b/tests/custom/03_stdlib/44_wildcard
new file mode 100644
index 0000000..d838e47
--- /dev/null
+++ b/tests/custom/03_stdlib/44_wildcard
@@ -0,0 +1,43 @@
+The `wildcard()` function tests whether the given wildcard pattern matches
+the given subject, optionally ignoring letter case.
+
+Returns `true` if the pattern matches the subject.
+
+Returns `false` if the pattern does not match the subject.
+
+Returns `null` if the pattern argument is not a string value.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ // A simple glob pattern match
+ wildcard("file.txt", "*.txt"),
+
+ // Using `?` as single character placeholder and case folding
+ wildcard("2022-02-02_BACKUP.LIST", "????-??-??_backup.*", true),
+
+ // Using bracket expressions
+ wildcard("aaa_123_zzz", "[a-z][a-z][a-z]_???_*"),
+
+ // Using no meta characters at all
+ wildcard("test", "test"),
+
+ // No match yields `false`
+ wildcard("abc", "d*"),
+
+ // Invalid pattern value yields `null`
+ wildcard("true", true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ true,
+ true,
+ true,
+ true,
+ false,
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/45_sourcepath b/tests/custom/03_stdlib/45_sourcepath
new file mode 100644
index 0000000..92c63bf
--- /dev/null
+++ b/tests/custom/03_stdlib/45_sourcepath
@@ -0,0 +1,69 @@
+The `sourcepath()` function determines the path of the currently executed
+ucode script file, optionally only the directory portion.
+
+By specifying the a depth parameter, the owning files of functions further
+up the call stack can be determined.
+
+Returns a string containing the path (or directory) of the running ucode
+source file.
+
+Returns `null` if the path is indeterminate.
+
+-- Testcase --
+{%
+ let output = render("files/include/level1.uc");
+
+ // replace dynamic testfiles path with placeholder for stable output
+ output = replace(output, TESTFILES_PATH, "...");
+
+ print(output);
+%}
+-- End --
+
+-- File include/level1.uc --
+This is the 1st level include.
+
+{% include("level2.uc") %}
+-- End --
+
+-- File include/level2.uc --
+This is the 2nd level include.
+
+{% include("level3.uc") %}
+-- End --
+
+-- File include/level3.uc --
+This is the 3rd level include.
+
+{% for (let depth in [0, 1, 2, 3]): %}
+Depth {{ depth }}:
+ Path: {{ sourcepath(depth, false) || "indeterminate" }}
+ Directory: {{ sourcepath(depth, true) || "indeterminate" }}
+
+{% endfor %}
+-- End --
+
+-- Expect stdout --
+This is the 1st level include.
+
+This is the 2nd level include.
+
+This is the 3rd level include.
+
+Depth 0:
+ Path: .../include/level3.uc
+ Directory: .../include
+
+Depth 1:
+ Path: .../include/level2.uc
+ Directory: .../include
+
+Depth 2:
+ Path: .../include/level1.uc
+ Directory: .../include
+
+Depth 3:
+ Path: indeterminate
+ Directory: indeterminate
+
+-- End --
diff --git a/tests/custom/03_stdlib/46_min b/tests/custom/03_stdlib/46_min
new file mode 100644
index 0000000..c07fbff
--- /dev/null
+++ b/tests/custom/03_stdlib/46_min
@@ -0,0 +1,28 @@
+The `min()` function returns the minimum of all given arguments.
+
+If multiple equivalent minimum values are given (e.g. `null` and `false`
+both are treated as `0` when comparing numerically), the first minimal
+value is returned.
+
+Returns the minimum value among all given arguments or `null` if no
+arguments were passed.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ min(),
+ min(5, 1, 3, -10),
+ min("foo", "bar", "xxx", "abc"),
+ min(false, null, 0, NaN)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ null,
+ -10,
+ "abc",
+ false
+]
+-- End --
diff --git a/tests/custom/03_stdlib/47_max b/tests/custom/03_stdlib/47_max
new file mode 100644
index 0000000..69e6fb8
--- /dev/null
+++ b/tests/custom/03_stdlib/47_max
@@ -0,0 +1,28 @@
+The `max()` function returns the maximum of all given arguments.
+
+If multiple equivalent maximum values are given (e.g. `null` and `false`
+both are treated as `0` when comparing numerically), the first maximal
+value is returned.
+
+Returns the maximum value among all given arguments or `null` if no
+arguments were passed.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ max(),
+ max(5, 1, 3, -10),
+ max("foo", "bar", "xxx", "abc"),
+ max(false, null, 0, NaN)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ null,
+ 5,
+ "xxx",
+ false
+]
+-- End --
diff --git a/tests/custom/03_stdlib/48_b64dec b/tests/custom/03_stdlib/48_b64dec
new file mode 100644
index 0000000..898aa7a
--- /dev/null
+++ b/tests/custom/03_stdlib/48_b64dec
@@ -0,0 +1,31 @@
+The `b64dec()` function decodes the given base64 input string.
+
+Returns a string containing the decoded data.
+
+Returns `null` if the input is not a string or if the input string was
+invalid base64 data (e.g. missing padding or non-whitespace characters
+outside the expected alphabet).
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ b64dec("SGVsbG8sIHdvcmxkIQ=="),
+ b64dec("SGVsbG8sIHdvcmxkIQ"),
+ b64dec("AAECAw=="),
+ b64dec("xxx"),
+ b64dec("==="),
+ b64dec(true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "Hello, world!",
+ null,
+ "\u0000\u0001\u0002\u0003",
+ null,
+ null,
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/49_b64enc b/tests/custom/03_stdlib/49_b64enc
new file mode 100644
index 0000000..3ef0381
--- /dev/null
+++ b/tests/custom/03_stdlib/49_b64enc
@@ -0,0 +1,25 @@
+The `b64enc()` function encodes the given input string as base64.
+
+Returns a string containing the encoded data.
+
+Returns `null` if the input is not a string.
+
+-- Testcase --
+{%
+ printf("%.J\n", [
+ b64enc("Hello, world!"),
+ b64enc("\u0000\u0001\u0002\u0003"),
+ b64enc(""),
+ b64enc(true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ "SGVsbG8sIHdvcmxkIQ==",
+ "AAECAw==",
+ "",
+ null
+]
+-- End --
diff --git a/tests/custom/03_stdlib/50_uniq b/tests/custom/03_stdlib/50_uniq
new file mode 100644
index 0000000..6e13077
--- /dev/null
+++ b/tests/custom/03_stdlib/50_uniq
@@ -0,0 +1,66 @@
+The `uniq()` function extracts the unique set of all values within the
+given input array, maintaining the original order.
+
+Returns an array containing all unique items of the input array.
+
+Returns `null` if the input is not an array value.
+
+-- Testcase --
+{%
+ let o1 = { an: "object" };
+ let o2 = { an: "object" }; // same but not identical
+
+ let a1 = [ 1, 2, 3 ];
+ let a2 = [ 1, 2, 3 ]; // same but not identical
+
+ printf("%.J\n", [
+ // strict comparison is used, 0 and "0" are not unique
+ uniq([ 0, 1, 2, 0, "0", 2, 3, "4", 4 ]),
+
+ // despite NaN != NaN, two NaN values are not unique
+ uniq([ NaN, NaN ]),
+
+ // only identical objects are filtered, not equivalent ones
+ uniq([ o1, o1, o2, a1, a1, a2 ]),
+
+ // invalid input yields `null`
+ uniq(true)
+ ]);
+%}
+-- End --
+
+-- Expect stdout --
+[
+ [
+ 0,
+ 1,
+ 2,
+ "0",
+ 3,
+ "4",
+ 4
+ ],
+ [
+ "NaN"
+ ],
+ [
+ {
+ "an": "object"
+ },
+ {
+ "an": "object"
+ },
+ [
+ 1,
+ 2,
+ 3
+ ],
+ [
+ 1,
+ 2,
+ 3
+ ]
+ ],
+ null
+]
+-- End --