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");
}