App-Ariza.git | runner/tests/ | test_config.c
/* The sidecar grammar: what a generated bin/<exec>.ariza has to parse
* to, in what order, and what a damaged one has to be refused for. */
#include <stdlib.h>
#include "support.h"
/* Assert one environment directive by position, because position is the
* contract: two prepend-path lines have to land in the order they were
* written, and a set after an unset of the same variable has to win. */
static void check_op(const arz_config *cfg, size_t i, arz_op_kind kind,
const char *name, const char *value)
{
if (i >= cfg->op_count) {
fprintf(stderr, "FAIL no directive at index %lu (of %lu)\n",
(unsigned long)i, (unsigned long)cfg->op_count);
t_checks++;
t_fails++;
return;
}
T_CHECK(cfg->ops[i].kind == kind);
if (name == NULL)
T_CHECK(cfg->ops[i].name == NULL);
else
T_EQ(cfg->ops[i].name, name);
if (value == NULL)
T_CHECK(cfg->ops[i].value == NULL);
else
T_EQ(cfg->ops[i].value, value);
}
/* The file ariza actually renders, parsed by the code that actually
* reads it.
*
* t/golden/launcher-windows-x86_64.ariza is the committed output of
* resources/templates/launcher-windows.ariza.j2, compared byte for byte
* by the Raku suite; parsing it here closes the loop across the two
* languages, so a template edit that changes a directive fails on this
* side rather than on a user's machine. ARIZA_GOLDEN_SIDECAR is set by
* CMakeLists.txt. */
static void test_golden_sidecar(void)
{
char *bytes = t_read_file(ARIZA_GOLDEN_SIDECAR);
arz_config cfg;
size_t line = 0;
if (bytes == NULL) {
fprintf(stderr, "FAIL cannot read %s\n", ARIZA_GOLDEN_SIDECAR);
t_checks++;
t_fails++;
return;
}
T_CHECK(arz_config_parse(t_u8(bytes), &cfg, &line) == ARZ_OK);
T_CHECK(line == 0);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_NONE);
T_EQ(cfg.target,
"rakudo\\share\\perl6\\vendor\\bin\\exampleapp.raku");
T_EQ(cfg.app_display, "Example App");
T_EQ(cfg.app_exec, "exampleapp");
/* The choreography every ariza bundle carries, which the .cmd
* launcher spells in batch and this spells as directives. Both
* have to produce the same environment, and this is the half of
* that a test can hold still. */
T_CHECK(cfg.op_count == 5);
check_op(&cfg, 0, ARZ_OP_SET, "RAKULIB",
"inst#{root}\\rakudo\\share\\perl6\\vendor");
check_op(&cfg, 1, ARZ_OP_UNSET, "PERL6LIB", NULL);
check_op(&cfg, 2, ARZ_OP_SET, "NOTCURSES_NATIVE_DATA_DIR",
"{root}\\native");
check_op(&cfg, 3, ARZ_OP_PREPEND_PATH, NULL, "{root}\\native\\sqlcipher");
check_op(&cfg, 4, ARZ_OP_SET, "DBIISH_SQLCIPHER_LIB",
"{root}\\native\\sqlcipher\\sqlcipher.dll");
arz_config_free(&cfg);
free(bytes);
}
/* The same shape as a literal, so the grammar is visible where the cases
* that exercise it are — CRLF, comment header, blank line. */
static const char *RENDERED =
"# Example App 9.9.9 -- ariza runner configuration.\r\n"
"#\r\n"
"# Read by bin\\exampleapp.exe. Generated by ariza; edit the bundle,\r\n"
"# not this file.\r\n"
"\r\n"
"target site\\bin\\exampleapp.raku\r\n"
"app-exec exampleapp\r\n"
"app-display Example App\r\n"
"set RAKULIB=inst#{root}\\site\r\n"
"unset PERL6LIB\r\n";
static void test_rendered(void)
{
arz_config cfg;
T_CHECK(arz_config_parse(t_u8(RENDERED), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_NONE);
T_EQ(cfg.target, "site\\bin\\exampleapp.raku");
T_EQ(cfg.app_exec, "exampleapp");
T_EQ(cfg.app_display, "Example App");
T_CHECK(cfg.op_count == 2);
/* A '#' only starts a comment in the first non-blank column. An
* inline-comment rule would truncate `inst#{root}\site` into
* `inst`, which is a RAKULIB that names a relative directory and a
* bundle that recompiles its whole closure on every launch. */
check_op(&cfg, 0, ARZ_OP_SET, "RAKULIB", "inst#{root}\\site");
check_op(&cfg, 1, ARZ_OP_UNSET, "PERL6LIB", NULL);
arz_config_free(&cfg);
}
static void test_line_endings(void)
{
arz_config cfg;
/* LF, CRLF and trailing whitespace before the newline all describe
* the same bundle: the file is CRLF as shipped, and anything that
* has been through a text editor or a git checkout may not be. */
T_CHECK(arz_config_parse(t_s(
"target a.raku\n"
"app-display A\r\n"
"app-exec a \r\n"), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_NONE);
T_EQ(cfg.target, "a.raku");
T_EQ(cfg.app_exec, "a");
arz_config_free(&cfg);
/* No trailing newline at all. */
T_CHECK(arz_config_parse(t_s(
"target a.raku\napp-display A\napp-exec a"), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_NONE);
T_EQ(cfg.app_exec, "a");
arz_config_free(&cfg);
}
static void test_whitespace_and_comments(void)
{
arz_config cfg;
T_CHECK(arz_config_parse(t_s(
" # an indented comment\n"
"\t \n"
" target site\\bin\\a b.raku \n"
"app-display A Tool\n"
"app-exec\ta\n"), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_NONE);
/* Trimmed around, kept inside: a display name and a path both
* legitimately contain runs of spaces, and a tab separates a
* directive from its argument as readily as a space. */
T_EQ(cfg.target, "site\\bin\\a b.raku");
T_EQ(cfg.app_display, "A Tool");
T_EQ(cfg.app_exec, "a");
arz_config_free(&cfg);
/* '#' mid-line is an ordinary character everywhere, not just in a
* `set` value. */
T_CHECK(arz_config_parse(t_s(
"target a.raku\napp-display C# for cats\napp-exec a\n"),
&cfg, NULL) == ARZ_OK);
T_EQ(cfg.app_display, "C# for cats");
arz_config_free(&cfg);
}
static void test_values(void)
{
arz_config cfg;
/* Non-ASCII survives byte for byte (unit for unit, on Windows):
* app-display goes straight to the user's terminal, and a bundle
* for an app with an accent in its name is not a special case. */
T_CHECK(arz_config_parse(t_u8(
"target site\\bin\\café.raku\n"
"app-display Caffè Nero — 日本語\n"
"app-exec cafe\n"
"set APP_HOME={root}\\café\n"), &cfg, NULL) == ARZ_OK);
T_EQ(cfg.target, "site\\bin\\café.raku");
T_EQ(cfg.app_display, "Caffè Nero — 日本語");
check_op(&cfg, 0, ARZ_OP_SET, "APP_HOME", "{root}\\café");
arz_config_free(&cfg);
/* A repeated metadata directive takes its last value; repeated
* environment directives accumulate, because order is their whole
* meaning. */
T_CHECK(arz_config_parse(t_s(
"target first.raku\napp-display A\napp-exec a\n"
"target second.raku\n"
"prepend-path {root}\\one\nprepend-path {root}\\two\n"),
&cfg, NULL) == ARZ_OK);
T_EQ(cfg.target, "second.raku");
T_CHECK(cfg.op_count == 2);
check_op(&cfg, 0, ARZ_OP_PREPEND_PATH, NULL, "{root}\\one");
check_op(&cfg, 1, ARZ_OP_PREPEND_PATH, NULL, "{root}\\two");
arz_config_free(&cfg);
/* An '=' in a value belongs to the value: the variable name ends at
* the first one and nothing after it is significant. */
T_CHECK(arz_config_parse(t_s(
"target a.raku\napp-display A\napp-exec a\n"
"set OPTS=--flag=1 --other=2\n"), &cfg, NULL) == ARZ_OK);
check_op(&cfg, 0, ARZ_OP_SET, "OPTS", "--flag=1 --other=2");
arz_config_free(&cfg);
/* An empty value is a set-but-empty variable, which is a different
* thing from an unset one — and the reason `unset` exists as its
* own directive rather than as `set NAME=`. */
T_CHECK(arz_config_parse(t_s(
"target a.raku\napp-display A\napp-exec a\nset QUIET=\n"),
&cfg, NULL) == ARZ_OK);
check_op(&cfg, 0, ARZ_OP_SET, "QUIET", "");
arz_config_free(&cfg);
/* A UTF-8 BOM, which nothing ariza writes and every Windows editor
* might. */
T_CHECK(arz_config_parse(t_u8(
"\xEF\xBB\xBFtarget a.raku\napp-display A\napp-exec a\n"),
&cfg, NULL) == ARZ_OK);
T_EQ(cfg.target, "a.raku");
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_NONE);
arz_config_free(&cfg);
}
static void test_many_ops(void)
{
arz_config cfg;
size_t i;
/* Past the growth step, so a bundle whose recipe set contributes a
* dozen lines is as ordinary as one that contributes two. */
T_CHECK(arz_config_parse(t_s(
"target a.raku\napp-display A\napp-exec a\n"
"set A=1\nset B=2\nset C=3\nset D=4\nset E=5\nset F=6\n"
"set G=7\nset H=8\nset I=9\nset J=10\nset K=11\nset L=12\n"),
&cfg, NULL) == ARZ_OK);
T_CHECK(cfg.op_count == 12);
for (i = 0; i < cfg.op_count; i++)
T_CHECK(cfg.ops[i].kind == ARZ_OP_SET);
check_op(&cfg, 0, ARZ_OP_SET, "A", "1");
check_op(&cfg, 11, ARZ_OP_SET, "L", "12");
arz_config_free(&cfg);
}
static void test_incomplete(void)
{
arz_config cfg;
T_CHECK(arz_config_parse(t_s(""), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_TARGET);
arz_config_free(&cfg);
T_CHECK(arz_config_parse(t_s("\r\n \n# nothing here\n"), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_TARGET);
arz_config_free(&cfg);
T_CHECK(arz_config_parse(NULL, &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_TARGET);
arz_config_free(&cfg);
T_CHECK(arz_config_parse(t_s("target a.raku\napp-exec a\n"), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_APP_DISPLAY);
arz_config_free(&cfg);
T_CHECK(arz_config_parse(t_s(
"target a.raku\napp-display A\n"), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_APP_EXEC);
arz_config_free(&cfg);
/* Environment directives are all optional: a bundle that needs no
* environment at all is unusual, not broken. */
T_CHECK(arz_config_parse(t_s(
"target a.raku\napp-display A\napp-exec a\n"), &cfg, NULL) == ARZ_OK);
T_CHECK(arz_config_missing(&cfg) == ARZ_KEY_NONE);
T_CHECK(cfg.op_count == 0);
arz_config_free(&cfg);
}
struct bad_case {
const char *text;
size_t line;
const char *why;
};
/* Every one of these is a bundle that would otherwise start with an
* environment that is quietly not the one it was built with. */
static const struct bad_case BAD[] = {
{ "target a.raku\nrehash-everything now\n", 2,
"a directive this runner does not implement — the case the whole"
" fail-closed rule exists for" },
{ "target a.raku\napp_display A\n", 2,
"a near miss on a directive name is still not one" },
{ "target\n", 1, "a directive with no argument" },
{ "target a.raku\ntarget\n", 2, "including on a repeat" },
{ "set\n", 1, "set with nothing at all" },
{ "set NAME\n", 1, "set with no '='" },
{ "set =value\n", 1, "set with no variable name" },
{ "unset\n", 1, "unset with nothing to unset" },
{ "prepend-path\n", 1, "prepend-path with no path" },
{ "set A={roots}\\x\n", 1,
"an unknown token: a typo that would otherwise reach the app as a"
" path that does not exist" },
{ "set A={root\n", 1, "an unterminated token" },
{ "prepend-path {ROOT}\\x\n", 1, "tokens are case-sensitive" },
{ "target a.raku\napp-display A\napp-exec a\nset A={}\n", 4,
"an empty token" },
};
static void test_syntax_errors(void)
{
size_t i;
for (i = 0; i < sizeof(BAD) / sizeof(BAD[0]); i++) {
arz_config cfg;
size_t line = 0;
arz_status st = arz_config_parse(t_s(BAD[i].text), &cfg, &line);
t_checks++;
if (st != ARZ_E_SYNTAX || line != BAD[i].line) {
t_fails++;
fprintf(stderr, "FAIL bad[%lu]: %s\n",
(unsigned long)i, BAD[i].why);
fprintf(stderr, " status %d (wanted %d), line %lu"
" (wanted %lu)\n", (int)st, (int)ARZ_E_SYNTAX,
(unsigned long)line, (unsigned long)BAD[i].line);
}
arz_config_free(&cfg);
}
}
static void test_key_names(void)
{
/* The names the win32 layer quotes back at a user are the names in
* the file. */
T_CHECK(t_eq(t_s(arz_key_name(ARZ_KEY_TARGET)), "target"));
T_CHECK(t_eq(t_s(arz_key_name(ARZ_KEY_APP_DISPLAY)), "app-display"));
T_CHECK(t_eq(t_s(arz_key_name(ARZ_KEY_APP_EXEC)), "app-exec"));
T_CHECK(t_eq(t_s(arz_key_name(ARZ_KEY_NONE)), ""));
}
int main(void)
{
test_golden_sidecar();
test_rendered();
test_line_endings();
test_whitespace_and_comments();
test_values();
test_many_ops();
test_incomplete();
test_syntax_errors();
test_key_names();
return t_done("config");
}