diff --git a/tests/custom/03_stdlib/01_chr b/tests/custom/03_stdlib/01_chr new file mode 100644 index 00000000..17163e31 --- /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 00000000..344069aa --- /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 00000000..3d71aa77 --- /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 00000000..1f941d12 --- /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 00000000..350e9529 --- /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 00000000..400db34a --- /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 00000000..419970ca --- /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 00000000..a6b59233 --- /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 00000000..dac49c39 --- /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 00000000..46462a09 --- /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 00000000..1ae3cb1b --- /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 00000000..508838dd --- /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 00000000..3a69228d --- /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 00000000..243618e9 --- /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 00000000..176cd4ca --- /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 00000000..ccc235fe --- /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 00000000..f8fb9a24 --- /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 00000000..e31ce789 --- /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 00000000..0c7eed3f --- /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 00000000..173ae094 --- /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 00000000..a5aeed3d --- /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 00000000..8f24f3a7 --- /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 00000000..34b600e1 --- /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 00000000..263fd187 --- /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 00000000..2001322b --- /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 00000000..17b54f76 --- /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 00000000..3edcd483 --- /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 00000000..a2a6d27c --- /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 00000000..681f3f70 --- /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 00000000..5f1ae24b --- /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 00000000..61d82e11 --- /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 00000000..b0530440 --- /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 00000000..b662ae88 --- /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$2$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 + +### +The quick brown fox jumps over the lazy dog +### +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 00000000..ba8ad9f6 --- /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 00000000..6d808f2b --- /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 00000000..64ef08af --- /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 00000000..2c0ff24c --- /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 00000000..faf8aa57 --- /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: + +<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 00000000..2b2295ad --- /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 00000000..017a8214 --- /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 00000000..bca6f0d5 --- /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 00000000..e6200e90 --- /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 00000000..9809c87a --- /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 00000000..d838e47c --- /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 00000000..92c63bf5 --- /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 00000000..c07fbff6 --- /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 00000000..69e6fb84 --- /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 00000000..898aa7ac --- /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 00000000..3ef03810 --- /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 00000000..6e130773 --- /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 --