aboutsummaryrefslogtreecommitdiff
path: root/contrib/bc/build.pkg.rig
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/bc/build.pkg.rig')
-rw-r--r--contrib/bc/build.pkg.rig2345
1 files changed, 2345 insertions, 0 deletions
diff --git a/contrib/bc/build.pkg.rig b/contrib/bc/build.pkg.rig
new file mode 100644
index 000000000000..d607e8885737
--- /dev/null
+++ b/contrib/bc/build.pkg.rig
@@ -0,0 +1,2345 @@
+/*
+ * *****************************************************************************
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2018-2025 Gavin D. Howard and contributors.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * *****************************************************************************
+ *
+ * The build package file.
+ *
+ */
+
+/// The path to the safe install script.
+SAFE_INSTALL: str = path.join(src_dir, "scripts/safe-install.sh");
+
+/// The file mode for executables, as an argument to the safe install script.
+EXEC_INSTALL_MODE: str = "-Dm755";
+
+/// The file mode for man pages and other files, as an argument to the safe
+/// install script.
+MANPAGE_INSTALL_MODE: str = "-Dm644";
+
+// Save this.
+OS: str = platform.os;
+
+DESTDIR: str = str(config["destdir"]);
+
+EXECPREFIX: str = str(config["execprefix"]);
+EXECSUFFIX: str = str(config["execsuffix"]);
+
+/**
+ * Generates the true executable name for the given base name.
+ * @param name The base name of the executable.
+ * @return The true name of the executable, including prefix, suffix, and
+ extension.
+ */
+fn exe_name(name: str) -> str
+{
+ temp: str = EXECPREFIX +~ name +~ EXECSUFFIX;
+ return if OS == "Windows" { temp +~ ".exe"; } else { temp; };
+}
+
+/**
+ * Generates the default executable name for the given base name.
+ * @param name The base name of the executable.
+ * @return The true name of the executable, including prefix, suffix, and
+ extension.
+ */
+fn default_exe_name(name: str) -> str
+{
+ return if OS == "Windows" { name +~ ".exe"; } else { name; };
+}
+
+/**
+ * Generates the true library name for the given base name.
+ * @param name The base name of the library.
+ * @return The true name of the library, including prefix and extension.
+ */
+fn lib_name(name: str) -> str
+{
+ ext: str = if OS == "Windows" { ".lib"; } else { ".a"; };
+ return "lib" +~ name +~ ext;
+}
+
+BC_BIN: str = exe_name("bc");
+DC_BIN: str = exe_name("dc");
+LIBRARY: str = lib_name("libbcl");
+
+BC_MANPAGE: str = EXECPREFIX +~ "bc" +~ EXECSUFFIX +~ ".1";
+DC_MANPAGE: str = EXECPREFIX +~ "dc" +~ EXECSUFFIX +~ ".1";
+BCL_MANPAGE: str = "bcl.3";
+
+BCL_HEADER: str = "bcl.h";
+BCL_HEADER_PATH: str = path.join(src_dir, path.join("include", BCL_HEADER));
+PC_FILE: str = "bcl.pc";
+
+/**
+ * Returns the string value of the define for a prompt default define for an
+ * executable.
+ * @param name The base name of the executable.
+ * @return The string value of the compiler define for the prompt default.
+ */
+fn prompt(name: str) -> str
+{
+ opt: sym = sym(config[name +~ "/default_prompt"]);
+
+ ret: str =
+ if opt == @off
+ {
+ "0";
+ }
+ else if opt == @tty_mode
+ {
+ str(uint(bool(config[name +~ "/default_tty_mode"])));
+ }
+ else
+ {
+ "1";
+ };
+
+ return ret;
+}
+
+HEADERS: []str = find_src_ext("include", "h");
+
+FORCE: bool = bool(config["force"]);
+
+BUILD_MODE: sym = sym(config["build_mode"]);
+
+BC_ENABLED: str = str(uint(BUILD_MODE == @both || BUILD_MODE == @bc));
+DC_ENABLED: str = str(uint(BUILD_MODE == @both || BUILD_MODE == @dc));
+LIBRARY_ENABLED: str = str(uint(BUILD_MODE == @library));
+
+EXTRA_MATH_ENABLED: str = str(uint(bool(config["extra_math"])));
+
+HISTORY: sym = sym(config["history"]);
+HISTORY_ENABLED: str = str(uint(HISTORY != @none));
+EDITLINE_ENABLED: str = str(uint(HISTORY == @editline));
+READLINE_ENABLED: str = str(uint(HISTORY == @readline));
+
+NLS_ENABLED: str =
+if OS == "Windows" || BUILD_MODE == @library
+{
+ "0";
+}
+else
+{
+ str(uint(sym(config["locales"]) != @none));
+};
+
+BUILD_TYPE: str =
+if EXTRA_MATH_ENABLED != "0" && HISTORY_ENABLED != "0" && NLS_ENABLED != "0"
+{
+ "A";
+}
+else
+{
+ t: str = if EXTRA_MATH_ENABLED != "0" { ""; } else { "E"; } +~
+ if HISTORY_ENABLED != "0" { ""; } else { "H"; } +~
+ if NLS_ENABLED != "0" { ""; } else { "N"; };
+
+ t;
+};
+
+OPTIMIZE: str = str(config["optimization"]);
+
+VALGRIND_ARGS: []str = @[
+ "valgrind",
+ "--error-exitcode=100",
+ "--leak-check=full",
+ "--show-leak-kinds=all",
+ "--errors-for-leak-kinds=all",
+ "--track-fds=yes",
+ "--track-origins=yes",
+];
+
+// Get the compiler. The user might have set one at the command line.
+CC: str = language.compiler;
+
+// Set optimization to "0" if it is empty.
+CFLAGS_OPT: str = if OPTIMIZE == "" { "0"; } else { OPTIMIZE; };
+
+// Get the command-line option for defining a preprocessor variable.
+DEFOPT: str = compiler_db["opt.define"];
+
+// Get the command-line string for the optimization option for the compiler.
+OPTOPT: str = compiler_db["opt.optimization"] +~ CFLAGS_OPT;
+
+// Get the compiler option for the object file to output to.
+OBJOUTOPT: str = compiler_db["opt.objout"];
+EXEOUTOPT: str = compiler_db["opt.exeout"];
+
+// Get the compiler option for outputting an object file rather than an
+// executable.
+OBJOPT: str = compiler_db["opt.obj"];
+
+// Get the compiler option for setting an include directory.
+INCOPT: str = compiler_db["opt.include"] +~ path.join(src_dir, "include");
+
+COVERAGE_CFLAGS: []str =
+if bool(config["coverage"])
+{
+ @[ "-fprofile-arcs", "-ftest-coverage", "-g", "-O0", DEFOPT +~ "NDEBUG" ];
+};
+
+MAINEXEC: str =
+if BUILD_MODE == @both || BUILD_MODE == @bc || BUILD_MODE == @library
+{
+ BC_BIN;
+}
+else
+{
+ DC_BIN;
+};
+
+MAINEXEC_FLAGS: []str = @[ DEFOPT +~ "MAINEXEC=" +~ MAINEXEC ];
+
+// XXX: Library needs these defines to be true.
+BC_DEF: str = if LIBRARY_ENABLED == "0" { BC_ENABLED; } else { "1"; };
+DC_DEF: str = if LIBRARY_ENABLED == "0" { DC_ENABLED; } else { "1"; };
+
+CFLAGS1: []str = config_list["cflags"] +~ @[ OPTOPT, INCOPT ] +~
+ COVERAGE_CFLAGS +~ MAINEXEC_FLAGS;
+CFLAGS2: []str = @[
+ DEFOPT +~ "BC_ENABLED=" +~ BC_DEF,
+ DEFOPT +~ "DC_ENABLED=" +~ DC_DEF,
+ DEFOPT +~ "BUILD_TYPE=" +~ BUILD_TYPE,
+ DEFOPT +~ "EXECPREFIX=" +~ str(config["execprefix"]),
+ DEFOPT +~ "BC_NUM_KARATSUBA_LEN=" +~ str(num(config["karatsuba_len"])),
+ DEFOPT +~ "BC_ENABLE_LIBRARY=" +~ LIBRARY_ENABLED,
+ DEFOPT +~ "BC_ENABLE_NLS=" +~ NLS_ENABLED,
+ DEFOPT +~ "BC_ENABLE_EXTRA_MATH=" +~ EXTRA_MATH_ENABLED,
+ DEFOPT +~ "BC_ENABLE_HISTORY=" +~ HISTORY_ENABLED,
+ DEFOPT +~ "BC_ENABLE_EDITLINE=" +~ EDITLINE_ENABLED,
+ DEFOPT +~ "BC_ENABLE_READLINE=" +~ READLINE_ENABLED,
+ DEFOPT +~ "BC_ENABLE_MEMCHECK=" +~ str(uint(bool(config["memcheck"]))),
+ DEFOPT +~ "BC_ENABLE_AFL=" +~ str(uint(bool(config["afl"]))),
+ DEFOPT +~ "BC_ENABLE_OSSFUZZ=" +~ str(uint(bool(config["ossfuzz"]))),
+ DEFOPT +~ "BC_DEFAULT_BANNER=" +~
+ str(uint(bool(config["bc/default_banner"]))),
+ DEFOPT +~ "BC_DEFAULT_SIGINT_RESET=" +~
+ str(uint(bool(config["bc/default_sigint_reset"]))),
+ DEFOPT +~ "BC_DEFAULT_TTY_MODE=" +~
+ str(uint(bool(config["bc/default_tty_mode"]))),
+ DEFOPT +~ "BC_DEFAULT_PROMPT=" +~ prompt("bc"),
+ DEFOPT +~ "BC_DEFAULT_EXPR_EXIT=" +~
+ str(uint(bool(config["bc/default_expr_exit"]))),
+ DEFOPT +~ "BC_DEFAULT_DIGIT_CLAMP=" +~
+ str(uint(bool(config["bc/default_digit_clamp"]))),
+ DEFOPT +~ "DC_DEFAULT_SIGINT_RESET=" +~
+ str(uint(bool(config["dc/default_sigint_reset"]))),
+ DEFOPT +~ "DC_DEFAULT_TTY_MODE=" +~
+ str(uint(bool(config["dc/default_tty_mode"]))),
+ DEFOPT +~ "DC_DEFAULT_PROMPT=" +~ prompt("dc"),
+ DEFOPT +~ "DC_DEFAULT_EXPR_EXIT=" +~
+ str(uint(bool(config["dc/default_expr_exit"]))),
+ DEFOPT +~ "DC_DEFAULT_DIGIT_CLAMP=" +~
+ str(uint(bool(config["dc/default_digit_clamp"]))),
+];
+CFLAGS: []str = CFLAGS1 +~ CFLAGS2;
+
+LDFLAGS: []str = config_list["ldflags"];
+
+COMMON_C_FILES: []str = @[
+ "src/data.c",
+ "src/num.c",
+ "src/rand.c",
+ "src/vector.c",
+ "src/vm.c",
+];
+
+EXEC_C_FILES: []str = @[
+ "src/args.c",
+ "src/file.c",
+ "src/lang.c",
+ "src/lex.c",
+ "src/main.c",
+ "src/opt.c",
+ "src/parse.c",
+ "src/program.c",
+ "src/read.c",
+];
+
+BC_C_FILES: []str = @[
+ "src/bc.c",
+ "src/bc_lex.c",
+ "src/bc_parse.c",
+];
+
+DC_C_FILES: []str = @[
+ "src/dc.c",
+ "src/dc_lex.c",
+ "src/dc_parse.c",
+];
+
+HISTORY_C_FILES: []str = @[
+ "src/history.c",
+];
+
+LIBRARY_C_FILES: []str = @[
+ "src/library.c",
+];
+
+GEN_HEADER1: str =
+ "// Copyright (c) 2018-2025 Gavin D. Howard and contributors.\n" +~
+ "// Licensed under the 2-clause BSD license.\n" +~
+ "// *** AUTOMATICALLY GENERATED FROM ";
+GEN_HEADER2: str = ". DO NOT MODIFY. ***\n\n";
+
+GEN_LABEL1: str = "const char *";
+GEN_LABEL2: str = " = \"";
+GEN_LABEL3: str = "\";\n\n";
+GEN_NAME1: str = "const char ";
+GEN_NAME2: str = "[] = {\n";
+
+GEN_LABEL_EXTERN1: str = "extern const char *";
+GEN_LABEL_EXTERN2: str = ";\n\n";
+GEN_NAME_EXTERN1: str = "extern const char ";
+GEN_NAME_EXTERN2: str = "[];\n\n";
+
+GEN_IFDEF1: str = "#if ";
+GEN_IFDEF2: str = "\n";
+GEN_ENDIF1: str = "#endif // ";
+GEN_ENDIF2: str = "\n";
+
+GEN_EX_START: str = "{{ A H N HN }}";
+GEN_EX_END: str = "{{ end }}";
+
+/// This is the max width to print characters to strgen files. This is to ensure
+/// that lines don't go much over 80 characters.
+MAX_WIDTH: usize = usize(72);
+
+/**
+ * A function to generate a C file that contains a C character array with the
+ * contents of a text file. For more detail, see the `gen/strgen.c` program;
+ * this function is exactly equivalent to that or should be.
+ * @param input The input file name.
+ * @param output The output file name.
+ * @param exclude True if extra math stuff should be excluded, false if
+ * they should be included.
+ * @param name The name of the array.
+ * @param label If not equal to "", this is the label for the array,
+ * which is essentially the "file name" in `bc` and `dc`.
+ * @param define If not equal to "", this is the preprocessor define
+ * expression that should be used to guard the array with a
+ * `#if`/`#endif` combo.
+ * @param remove_tabs True if tabs should be ignored, false if they should be
+ * included.
+ */
+fn strgen(
+ input: str,
+ output: str,
+ exclude: bool,
+ name: str,
+ label: str,
+ def: str,
+ remove_tabs: bool,
+) -> void
+{
+ in: str = io.read_file(input);
+
+ io.open(output, "w"): f
+ {
+ f.print(GEN_HEADER1 +~ input +~ GEN_HEADER2);
+
+ if label != ""
+ {
+ f.print(GEN_LABEL_EXTERN1 +~ label +~ GEN_LABEL_EXTERN2);
+ }
+
+ f.print(GEN_NAME_EXTERN1 +~ name +~ GEN_NAME_EXTERN2);
+
+ if def != ""
+ {
+ f.print(GEN_IFDEF1 +~ def +~ GEN_IFDEF2);
+ }
+
+ if label != ""
+ {
+ f.print(GEN_LABEL1 +~ label +~ GEN_LABEL2 +~ name +~ GEN_LABEL3);
+ }
+
+ f.print(GEN_NAME1 +~ name +~ GEN_NAME2);
+
+ i: !usize = usize(0);
+ count: !usize = usize(0);
+ slashes: !usize = usize(0);
+
+ // This is where the end of the license comment is found.
+ while slashes < 2 && in[i] > 0
+ {
+ if slashes == 1 && in[i] == '*' && in[i + 1] == '/' &&
+ (in[i + 2] == '\n' || in[i + 2] == '\r')
+ {
+ slashes! = slashes + usize(1);
+ i! = i + usize(2);
+ }
+ else if slashes == 0 && in[i] == '/' && in[i + 1] == '*'
+ {
+ slashes! = slashes + usize(1);
+ i! = i + usize(1);
+ }
+
+ i! = i + usize(1);
+ }
+
+ // The file is invalid if the end of the license comment could not be
+ // found.
+ if i == in.len
+ {
+ error("Could not find end of license comment");
+ }
+
+ i! = i + usize(1);
+
+ // Do not put extra newlines at the beginning of the char array.
+ while in[i] == '\n' || in[i] == '\r'
+ {
+ i! = i + usize(1);
+ }
+
+ // This loop is what generates the actual char array. It counts how many
+ // chars it has printed per line in order to insert newlines at
+ // appropriate places. It also skips tabs if they should be removed.
+ while i < in.len
+ {
+ if in[i] == '\r'
+ {
+ i! = i + usize(1);
+ continue;
+ }
+
+ // If we should output the character, i.e., it is not a tab or we
+ // can remove tabs...
+ if !remove_tabs || in[i] != '\t'
+ {
+ // Check for excluding something for extra math.
+ if in[i] == '{'
+ {
+ if i + GEN_EX_START.len <= in.len &&
+ in.slice(i, i + GEN_EX_START.len) == GEN_EX_START
+ {
+ if exclude
+ {
+ // Get past the braces.
+ i! = i + usize(2);
+
+ // Find the end of the end.
+ while in[i] != '{' &&
+ in.slice(i, i + GEN_EX_END.len) != GEN_EX_END
+ {
+ i! = i + usize(1);
+ }
+
+ i! = i + GEN_EX_END.len;
+
+ // Skip the last newline.
+ if in[i] == '\r'
+ {
+ i! = i + usize(1);
+ }
+
+ i! = i + usize(1);
+
+ continue;
+ }
+ }
+ else if !exclude &&
+ in.slice(i, i + GEN_EX_END.len) == GEN_EX_END
+ {
+ i! = i + GEN_EX_END.len;
+
+ // Skip the last newline.
+ if in[i] == '\r'
+ {
+ i! = i + usize(1);
+ }
+
+ i! = i + usize(1);
+
+ continue;
+ }
+ }
+
+ // Print a tab if we are at the beginning of a line.
+ if count == 0
+ {
+ f.print("\t");
+ }
+
+ val: str = str(in[i]) +~ ",";
+
+ // Print the character.
+ f.print(val);
+
+ // Adjust the count.
+ count! = count + val.len;
+
+ if count > MAX_WIDTH
+ {
+ count! = usize(0);
+ f.print("\n");
+ }
+ }
+
+ i! = i + usize(1);
+ }
+
+ // Make sure the end looks nice.
+ if count == 0
+ {
+ f.print(" ");
+ }
+
+ // Insert the NUL byte at the end.
+ f.print("0\n};\n");
+
+ if def != ""
+ {
+ f.print(GEN_ENDIF1 +~ def +~ GEN_ENDIF2);
+ }
+ }
+}
+
+/**
+ * Creates a target to generate an object file from the given C file and returns
+ * the target name of the new target.
+ * @param c_file The name of the C file target.
+ * @return The name of the object file target.
+ */
+fn c2o(c_file: str) -> str
+{
+ o_file: str = c_file +~ (if OS == "Windows" { ".obj"; } else { ".o"; });
+
+ target o_file: c_file, HEADERS
+ {
+ $ $CC %(config_list["other_cflags"]) %(CFLAGS) $OBJOPT $OBJOUTOPT @(tgt)
+ @(file_dep);
+ }
+
+ return o_file;
+}
+
+/**
+ * Generates a target to turn a text file into a C file with the text file's
+ * contents as a char array, then generates a target to generate an object file
+ * from that C file, then returns the name of the object file target.
+ * @param txt_file The name of the text file.
+ * @param name The name of the char array in the C file.
+ * @param label The label for the array, if any. (See the @a strgen()
+ * function for more information.)
+ * @param def The preprocessor define(s) to guard the array, if any.
+ * (See the @a strgen() function for more information.)
+ * @param remove_tabs True if tabs should be ignored, false otherwise. (See the
+ * @a strgen() function for more information.)
+ * @return The name of the object file target.
+ */
+fn txt2o(
+ txt_file: str,
+ name: str,
+ label: str,
+ def: str,
+ remove_tabs: bool,
+) -> str
+{
+ c_file: str = txt_file +~ ".c";
+
+ c_config: Gaml = @(gaml){
+ strgen_name: $name
+ strgen_label: $label
+ strgen_define: $def
+ strgen_remove_tabs: $remove_tabs
+ };
+
+ push c_config: config_stack
+ {
+ target c_file: txt_file
+ {
+ strgen(file_dep, tgt, EXTRA_MATH_ENABLED == "0",
+ str(config["strgen_name"]), str(config["strgen_label"]),
+ str(config["strgen_define"]),
+ bool(config["strgen_remove_tabs"]));
+ }
+ }
+
+ return c2o(c_file);
+}
+
+/**
+ * Generates a target for an executable and returns its name.
+ * @param name The name of the executable.
+ * @param o_files The object files for the executable.
+ * @return The name of the generated target.
+ */
+fn exe(name: str, o_files: []str) -> void
+{
+ target name: o_files
+ {
+ $ $CC %(config_list["other_cflags"]) %(config_list["strip_flag"])
+ %(CFLAGS) %(LDFLAGS) $EXEOUTOPT @(tgt) %(file_deps);
+ }
+}
+
+/**
+ * Generates a target for a link.
+ * @param name The name of the link.
+ * @param exec The name of the executable target.
+ */
+fn ln(name: str, exec: str) -> void
+{
+ if OS == "Windows"
+ {
+ target name: exec
+ {
+ $ copy /v /y /b @(file_dep) @(tgt);
+ }
+ }
+ else
+ {
+ target name: exec
+ {
+ $ ln -fs @("./" +~ path.basename(file_dep)) @(tgt);
+ }
+ }
+}
+
+/**
+ * Generates a target for a library.
+ * @param name The name of the library.
+ * @param exec The name of the executable target.
+ */
+fn lib(name: str, o_files: []str) -> void
+{
+ if OS == "WINDOWS"
+ {
+ exe(name, o_files);
+ }
+ else
+ {
+ target name: o_files
+ {
+ $ ar -r -cu @(tgt) %(file_deps);
+ }
+ }
+}
+
+fn check_err_test(
+ name: str,
+ res: CmdResult,
+) -> void
+{
+ if res.exitcode > 127
+ {
+ error("Test \"" +~ name +~ "\" crashed");
+ }
+
+ if res.exitcode == 0
+ {
+ error("Test \"" +~ name +~ "\" returned no error");
+ }
+
+ if res.exitcode == 100
+ {
+ error("Test \"" +~ name +~ "\" had memory errors on non-fatal error\n");
+ }
+
+ if res.stderr.len <= 1
+ {
+ error("Test \"" +~ name +~ "\" produced no error message");
+ }
+}
+
+fn check_test_retcode(
+ name: str,
+ exitcode: uint,
+) -> void
+{
+ if exitcode != 0
+ {
+ error("Test \"" +~ name +~ "\" failed with exitcode: " +~
+ str(exitcode) +~ "\n");
+ }
+}
+
+fn check_test(
+ name: str,
+ res: CmdResult,
+ exp_path: str,
+) -> void
+{
+ check_test_retcode(name, res.exitcode);
+
+ exp := io.read_file_bytes(exp_path);
+
+ if exp != res.stdout_full
+ {
+ error("Test \"" +~ name +~ "\" failed\n" +~ str(res.stderr));
+ }
+}
+
+fn register_standard_tests(
+ bin: str,
+ testdir: str,
+ src_testdir: str,
+ extra: bool,
+) -> void
+{
+ all_file: str = path.join(src_testdir, "all.txt");
+ tests: []str = io.read_file(all_file).split("\n");
+
+ extra_path := path.join(src_dir, "tests/extra_required.txt");
+ extra_required: []str = io.read_file(extra_path).split("\n");
+
+ for t: tests
+ {
+ if t == ""
+ {
+ continue;
+ }
+
+ // Skip extra math tests if it is not enabled.
+ if !extra && extra_required contains t
+ {
+ continue;
+ }
+
+ test sym(path.join(testdir, t)): bin
+ {
+ halt: str = str(config["halt"]);
+
+ name: str = path.basename(tgt_name);
+ testdir: str = path.dirname(tgt_name);
+ calc: str = path.basename(testdir);
+
+ test_file: str = tgt_name +~ ".txt";
+ test_result_file: str = tgt_name +~ "_results.txt";
+
+ src_test_file: str = path.join(src_dir, test_file);
+ src_test_result_file: str = path.join(src_dir, test_result_file);
+
+ actual_test_file: str =
+ if !path.isfile(src_test_file)
+ {
+ // If we shouldn't generate tests, skip.
+ if !bool(config["generated_tests"])
+ {
+ io.eprint("Skipping test " +~ tgt_name +~ "\n");
+ return;
+ }
+
+ script_name: str = name +~ "." +~ calc;
+
+ scriptdir: str = path.join(testdir, "scripts");
+ src_scriptdir: str = path.join(src_dir, scriptdir);
+ src_script_name: str = path.join(src_scriptdir, script_name);
+
+ $ @(default_exe_name(calc)) $src_script_name > $test_file;
+
+ test_file;
+ }
+ else
+ {
+ src_test_file;
+ };
+
+ exp_result_file: str =
+ if !path.isfile(src_test_result_file)
+ {
+ tmpfile: str = path.tmp(calc +~ "_test_result");
+
+ $ @(default_exe_name(calc)) %(config_list["gen_options"])
+ $actual_test_file << $halt > $tmpfile;
+
+ tmpfile;
+ }
+ else
+ {
+ src_test_result_file;
+ };
+
+ res := $ %(config_list["args"]) %(config_list["options"])
+ $actual_test_file << $halt;
+
+ check_test(tgt_name, res, exp_result_file);
+ }
+ }
+}
+
+fn register_script_tests(
+ bin: str,
+ testdir: str,
+ src_testdir: str,
+ extra: bool,
+) -> void
+{
+ scriptdir: str = path.join(testdir, "scripts");
+ src_scriptdir: str = path.join(src_testdir, "scripts");
+ all_file: str = path.join(src_scriptdir, "all.txt");
+ tests: []str = io.read_file(all_file).split("\n");
+
+ for t: tests
+ {
+ if t == ""
+ {
+ continue;
+ }
+
+ // Skip extra math tests if it is not enabled.
+ if !extra && (t == "rand.bc" || t == "root.bc" || t == "i2rand.bc")
+ {
+ continue;
+ }
+
+ test sym(path.join(scriptdir, t)): bin
+ {
+ halt: str = str(config["halt"]);
+
+ name: str = path.basename(tgt_name);
+ testdir: str = path.dirname(tgt_name);
+ testdir2: str = path.dirname(testdir);
+ calc: str = path.basename(testdir2);
+
+ test_file: str = tgt_name;
+ test_file_dir: str = path.dirname(tgt_name);
+ test_file_name: str = path.basename(tgt_name, "." +~ calc);
+ test_result_file: str = path.join(test_file_dir,
+ test_file_name +~ ".txt");
+
+ src_test_file: str = path.join(src_dir, test_file);
+ src_test_result_file: str = path.join(src_dir, test_result_file);
+
+ exp_result_file: str =
+ if !path.isfile(src_test_result_file)
+ {
+ tmpfile: str = path.tmp(calc +~ "_script_test_result");
+
+ // This particular test needs to be generated straight. Also, on
+ // Windows, we don't have `sed`, and the `bc`/`dc` there is
+ // probably this one anyway.
+ if name == "stream.dc" || host.os == "Windows"
+ {
+ $ @(default_exe_name(calc)) $src_test_file << $halt
+ > $tmpfile;
+ }
+ else
+ {
+ root_testdir: str = path.join(src_dir, "tests");
+
+ // This sed and the script are to remove an incompatibility
+ // with GNU bc, where GNU bc is wrong. See the development
+ // manual (manuals/development.md#script-tests) for more
+ // information.
+ $ @(default_exe_name(calc)) $src_test_file << $halt |
+ sed -n -f @(path.join(root_testdir, "script.sed"))
+ > $tmpfile;
+ }
+
+ tmpfile;
+ }
+ else
+ {
+ src_test_result_file;
+ };
+
+ if calc == "bc"
+ {
+ res1 := $ %(config_list["args"]) -g
+ %(config_list["script_options"]) $src_test_file
+ << $halt;
+
+ check_test(tgt_name, res1, exp_result_file);
+ }
+
+ // These tests do not need to run without global stacks.
+ if name == "globals.bc" || name == "references.bc" ||
+ name == "rand.bc"
+ {
+ return;
+ }
+
+ res2 := $ %(config_list["args"]) %(config_list["script_options"])
+ $src_test_file << $halt;
+
+ check_test(tgt_name, res2, exp_result_file);
+ }
+ }
+}
+
+fn register_stdin_test(
+ bin: str,
+ testdir: str,
+ name: str
+) -> void
+{
+ test sym(path.join(testdir, name)): bin
+ {
+ name: str = path.basename(tgt_name);
+ testdir: str = path.dirname(tgt_name);
+ calc: str = path.basename(testdir);
+
+ halt: str = if name == "bc" { "halt"; } else { "q"; };
+
+ test_file: str = tgt_name +~ ".txt";
+ test_result_file: str = tgt_name +~ "_results.txt";
+
+ src_test_file: str = path.join(src_dir, test_file);
+ src_test_result_file: str = path.join(src_dir, test_result_file);
+
+ res := $ %(config_list["args"]) %(config_list["options"])
+ < $src_test_file;
+
+ check_test(tgt_name, res, src_test_result_file);
+ }
+}
+
+fn register_stdin_tests(
+ bin: str,
+ testdir: str,
+ src_testdir: str,
+) -> void
+{
+ calc: str = path.basename(testdir);
+
+ if calc == "bc"
+ {
+ for t: @[ "stdin", "stdin1", "stdin2" ]
+ {
+ register_stdin_test(bin, testdir, t);
+ }
+ }
+ else
+ {
+ // dc only needs one.
+ register_stdin_test(bin, testdir, "stdin");
+ }
+}
+
+fn register_read_tests(
+ bin: str,
+ testdir: str,
+ src_testdir: str,
+) -> void
+{
+ calc: str = path.basename(testdir);
+
+ read_call: str = if calc == "bc" { "read()"; } else { "?"; };
+ read_expr: str =
+ if calc == "bc"
+ {
+ read_call +~ "\n5+5;";
+ }
+ else
+ {
+ read_call;
+ };
+ read_multiple: str =
+ if calc == "bc"
+ {
+ "3\n2\n1\n";
+ }
+ else
+ {
+ "3pR\n2pR\n1pR\n";
+ };
+
+ read_test_config: Gaml = @(gaml){
+ read_call: $read_call
+ read_expr: $read_expr
+ read_multiple: $read_multiple
+ };
+
+ push read_test_config: config_stack
+ {
+ // First test is the regular read test.
+ test sym(path.join(testdir, "read")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ test_file: str = tgt_name +~ ".txt";
+ src_test_file: str = path.join(src_dir, test_file);
+
+ read_call: str = str(config["read_call"]);
+
+ lines: []str = io.read_file(src_test_file).split("\n");
+
+ for l: lines
+ {
+ if l == ""
+ {
+ continue;
+ }
+
+ res := $ %(config_list["args"]) %(config_list["options"])
+ << @(read_call +~ "\n" +~ l +~ "\n");
+
+ check_test(tgt_name, res,
+ path.join(src_testdir, "read_results.txt"));
+ }
+ }
+
+ // Next test is reading multiple times.
+ test sym(path.join(testdir, "read_multiple")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+
+ test_file: str = tgt_name +~ ".txt";
+
+ path.mkdirp(path.dirname(test_file));
+
+ read_call: str = str(config["read_call"]);
+
+ exp_path: str = path.tmp("read_multiple_results");
+
+ io.open(exp_path, "w"): f
+ {
+ f.print("3\n2\n1\n");
+ }
+
+ res := $ %(config_list["args"]) %(config_list["options"])
+ -e $read_call -e $read_call -e $read_call
+ << @(str(config["read_multiple"]));
+
+ check_test(tgt_name, res, exp_path);
+ }
+
+ // Next test is the read errors test.
+ test sym(path.join(testdir, "read_errors")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ test_file: str = tgt_name +~ ".txt";
+ src_test_file: str = path.join(src_dir, test_file);
+
+ path.mkdirp(path.dirname(test_file));
+
+ read_call: str = str(config["read_call"]);
+
+ lines: []str = io.read_file(src_test_file).split("\n");
+
+ for l: lines
+ {
+ if l == ""
+ {
+ continue;
+ }
+
+ res := $ %(config_list["args"]) %(config_list["options"])
+ << @(read_call +~ "\n" +~ l +~ "\n");
+
+ check_err_test(tgt_name, res);
+ }
+ }
+
+ // Next test is the empty read test.
+ test sym(path.join(testdir, "read_empty")): bin
+ {
+ read_call: str = str(config["read_call"]);
+
+ res := $ %(config_list["args"]) %(config_list["options"])
+ << @(read_call +~ "\n");
+
+ check_err_test(tgt_name, res);
+ }
+
+ // Next test is the read EOF test.
+ test sym(path.join(testdir, "read_EOF")): bin
+ {
+ read_call: str = str(config["read_call"]);
+
+ res := $ %(config_list["args"]) %(config_list["options"])
+ << $read_call;
+
+ check_err_test(tgt_name, res);
+ }
+ }
+}
+
+fn run_error_lines_test(name: str) -> void
+{
+ file: str = path.join(src_dir, name);
+
+ lines: []str = io.read_file(file).split("\n");
+
+ for l: lines
+ {
+ if l == ""
+ {
+ continue;
+ }
+
+ res := $ %(config_list["args"]) %(config_list["options"])
+ %(config_list["error_options"]) << @(l +~ "\n");
+
+ check_err_test(name, res);
+ }
+}
+
+fn register_error_tests(
+ bin: str,
+ testdir: str,
+ src_testdir: str,
+) -> void
+{
+ calc: str = path.basename(testdir);
+
+ // First test is command-line expression error.
+ test sym(path.join(testdir, "command-line_expr_error")): bin
+ {
+ halt: str = str(config["halt"]);
+
+ res := $ %(config_list["args"]) %(config_list["options"]) -e "1+1" -f-
+ -e "2+2" << $halt;
+
+ check_err_test(tgt_name, res);
+ }
+
+ // First test is command-line file expression error.
+ test sym(path.join(testdir, "command-line_file_expr_error")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ halt: str = str(config["halt"]);
+
+ res := $ %(config_list["args"]) %(config_list["options"]) -e "1+1" -f-
+ -f @(path.join(testdir, "decimal.txt")) << $halt;
+
+ check_err_test(tgt_name, res);
+ }
+
+ if calc == "bc"
+ {
+ test sym(path.join(testdir, "posix_warning")): bin
+ {
+ res := $ %(config_list["args"]) %(config_list["options"]) -w
+ << @("line");
+
+ if res.exitcode != 0
+ {
+ error("Test \"" +~ tgt_name +~ "\" returned an error (" +~
+ str(res.exitcode) +~ ")");
+ }
+
+ output: str = str(res.stderr);
+
+ if output == "" || output == "\n"
+ {
+ error("Test \"" +~ tgt_name +~ "\" did not print a warning");
+ }
+ }
+
+ test sym(path.join(testdir, "posix_errors.txt")): bin
+ {
+ run_error_lines_test(tgt_name);
+ }
+ }
+
+ test sym(path.join(testdir, "errors.txt")): bin
+ {
+ run_error_lines_test(tgt_name);
+ }
+
+ errors_dir: str = path.join(testdir, "errors");
+
+ for f: find_src_ext(errors_dir, "txt")
+ {
+ // Skip the problematic test, if requested.
+ if calc == "bc" && f contains "33.txt" &&
+ !bool(config["problematic_tests"])
+ {
+ continue;
+ }
+
+ test sym(f): bin
+ {
+ errors_dir: str = path.dirname(tgt_name);
+ testdir: str = path.dirname(errors_dir);
+ calc: str = path.basename(testdir);
+
+ halt: str = str(config["halt"]);
+
+ res1 := $ %(config_list["args"]) %(config_list["error_options"]) -c
+ @(tgt_name) << $halt;
+
+ check_err_test(tgt_name, res1);
+
+ res2 := $ %(config_list["args"]) %(config_list["error_options"]) -C
+ @(tgt_name) << $halt;
+
+ check_err_test(tgt_name, res2);
+
+ res3 := $ %(config_list["args"]) %(config_list["error_options"]) -c
+ < @(path.join(src_dir, tgt_name));
+
+ check_err_test(tgt_name, res3);
+
+ res4 := $ %(config_list["args"]) %(config_list["error_options"]) -C
+ < @(path.join(src_dir, tgt_name));
+
+ check_err_test(tgt_name, res4);
+ }
+ }
+}
+
+fn check_kwredef_test(
+ name: str,
+ res: CmdResult,
+) -> void
+{
+ testdir: str = path.dirname(name);
+ redefine_exp: str = path.join(testdir, "redefine_exp.txt");
+
+ check_test(tgt_name, res, redefine_exp);
+}
+
+OTHER_LINE_LEN_RESULTS_NAME: str = "line_length_test_results.txt";
+OTHER_LINE_LEN70_RESULTS_NAME: str = "line_length70_test_results.txt";
+OTHER_MATHLIB_SCALE_RESULTS_NAME: str = "mathlib_scale_results.txt";
+
+fn register_other_tests(
+ bin: str,
+ testdir: str,
+ src_testdir: str,
+ extra: bool,
+) -> void
+{
+ calc: str = path.basename(testdir);
+
+ path.mkdirp(testdir);
+
+ // Halt test.
+ test sym(path.join(testdir, "halt")): bin
+ {
+ halt: str = str(config["halt"]) +~ "\n";
+
+ res := $ %(config_list["args"]) << $halt;
+
+ check_test_retcode(tgt_name, res.exitcode);
+ }
+
+ if calc == "bc"
+ {
+ // bc has two halt or quit commands, so test the second as well.
+ test sym(path.join(testdir, "quit")): bin
+ {
+ res := $ %(config_list["args"]) << @("quit\n");
+
+ check_test_retcode(tgt_name, res.exitcode);
+ }
+
+ // Also, make sure quit only quits after an expression.
+ test sym(path.join(testdir, "quit_after_expr")): bin
+ {
+ res := $ %(config_list["args"]) -e "1+1" << @("quit\n");
+
+ check_test_retcode(tgt_name, res.exitcode);
+
+ if str(res.stdout) != "2"
+ {
+ error("Test \"" +~ tgt_name +~
+ "\" did not have the right output");
+ }
+ }
+
+ test sym(path.join(testdir, "env_args1")): bin
+ {
+ env.set env.str("BC_ENV_ARGS", " '-l' '' -q")
+ {
+ res := $ %(config_list["args"]) << @("s(.02893)\n");
+
+ check_test_retcode(tgt_name, res.exitcode);
+ }
+ }
+
+ test sym(path.join(testdir, "env_args2")): bin
+ {
+ env.set env.str("BC_ENV_ARGS", " '-l' '' -q")
+ {
+ res := $ %(config_list["args"]) -e 4 << @("halt\n");
+
+ check_test_retcode(tgt_name, res.exitcode);
+ }
+ }
+
+ redefine_exp: str = path.join(testdir, "redefine_exp.txt");
+
+ io.open(redefine_exp, "w"): f
+ {
+ f.print("5\n0\n");
+ }
+
+ test sym(path.join(testdir, "keyword_redefinition1")): bin
+ {
+ res := $ %(config_list["args"]) --redefine=print -e
+ "define print(x) { x }" -e "print(5)" << @("halt\n");
+
+ check_kwredef_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "keyword_redefinition2")): bin
+ {
+ res := $ %(config_list["args"]) -r abs -r else -e
+ "abs = 5; else = 0" -e "abs;else" << @("halt\n");
+
+ check_kwredef_test(tgt_name, res);
+ }
+
+ if extra
+ {
+ test sym(path.join(testdir, "keyword_redefinition_lib2")): bin
+ {
+ res := $ %(config_list["args"]) -lr abs -e "perm(5, 1)" -e 0
+ << @("halt\n");
+
+ check_kwredef_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "leading_zero_script")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ res := $ %(config_list["args"]) -lz
+ @(path.join(src_testdir, "leadingzero.txt"))
+ << @(str(config["halt"]));
+
+ check_test(tgt_name, res,
+ path.join(src_testdir, "leadingzero_results.txt"));
+ }
+ }
+
+ test sym(path.join(testdir, "keyword_redefinition3")): bin
+ {
+ res := $ %(config_list["args"]) -r abs -r else -e
+ "abs = 5; else = 0" -e "abs;else" << @("halt\n");
+
+ check_kwredef_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "keyword_redefinition_error")): bin
+ {
+ res := $ %(config_list["args"]) -r break -e "define break(x) { x }";
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir,
+ "keyword_redefinition_without_redefine")): bin
+ {
+ res := $ %(config_list["args"]) -e "define read(x) { x }";
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "multiline_comment_in_expr_file")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ // tests/bc/misc1.txt happens to have a multiline comment in it.
+ src_test_file: str = path.join(src_testdir, "misc1.txt");
+ src_test_results_file: str = path.join(src_testdir,
+ "misc1_results.txt");
+
+ res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
+
+ check_test(tgt_name, res, src_test_results_file);
+ }
+
+ test sym(path.join(testdir,
+ "multiline_comment_error_in_expr_file")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ src_test_file: str = path.join(src_testdir, "errors/05.txt");
+
+ res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "multiline_string_in_expr_file")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ // tests/bc/strings.txt happens to have a multiline string in it.
+ src_test_file: str = path.join(src_testdir, "strings.txt");
+ src_test_results_file: str = path.join(src_testdir,
+ "strings_results.txt");
+
+ res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
+
+ check_test(tgt_name, res, src_test_results_file);
+ }
+
+ tst := path.join(testdir,
+ "multiline_string_with_backslash_error_in_expr_file");
+
+ test sym(tst): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ src_test_file: str = path.join(src_testdir, "errors/16.txt");
+
+ res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
+
+ check_err_test(tgt_name, res);
+ }
+
+ tst2 := path.join(testdir, "multiline_string_error_in_expr_file");
+
+ test sym(tst2): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ src_test_file: str = path.join(src_testdir, "errors/04.txt");
+
+ res := $ %(config_list["args"]) -f $src_test_file << @("halt\n");
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "interactive_halt")): bin
+ {
+ res := $ %(config_list["args"]) -i << @("halt\n");
+
+ check_test_retcode(tgt_name, res.exitcode);
+ }
+ }
+ else
+ {
+ test sym(path.join(testdir, "env_args1")): bin
+ {
+ env.set env.str("DC_ENV_ARGS", "'-x'"), env.str("DC_EXPR_EXIT", "1")
+ {
+ res := $ %(config_list["args"]) << @("4s stuff\n");
+
+ check_test_retcode(tgt_name, res.exitcode);
+ }
+ }
+
+ test sym(path.join(testdir, "env_args2")): bin
+ {
+ env.set env.str("DC_ENV_ARGS", "'-x'"), env.str("DC_EXPR_EXIT", "1")
+ {
+ res := $ %(config_list["args"]) -e 4pR;
+
+ check_test_retcode(tgt_name, res.exitcode);
+ }
+ }
+
+ test sym(path.join(testdir, "extended_register_command1")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ results: str = tgt_name +~ ".txt";
+
+ path.mkdirp(testdir);
+
+ io.open(results, "w"): f
+ {
+ f.print("0\n");
+ }
+
+ res := $ %(config_list["args"]) -e gxpR << @("q\n");
+
+ check_test(tgt_name, res, results);
+ }
+
+ test sym(path.join(testdir, "extended_register_command2")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ results: str = tgt_name +~ ".txt";
+
+ path.mkdirp(testdir);
+
+ io.open(results, "w"): f
+ {
+ f.print("1\n");
+ }
+
+ res := $ %(config_list["args"]) -x -e gxpR << @("q\n");
+
+ check_test(tgt_name, res, results);
+ }
+ }
+
+ path.mkdirp(testdir);
+
+ other_tests_results: []str = config_list["other_tests_results"];
+
+ io.open(path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME), "w"): f
+ {
+ f.print(other_tests_results[0] +~ "\n");
+ }
+
+ io.open(path.join(testdir, OTHER_LINE_LEN70_RESULTS_NAME), "w"): f
+ {
+ f.print(other_tests_results[1] +~ "\n");
+ }
+
+ test sym(path.join(testdir, "line_length1")): bin
+ {
+ env.set env.str(str(config["var"]), "80")
+ {
+ testdir: str = path.dirname(tgt_name);
+
+ other_tests: []str = config_list["other_tests"];
+
+ res := $ %(config_list["args"]) << @(other_tests[3]);
+
+ check_test(tgt_name, res,
+ path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME));
+ }
+ }
+
+ test sym(path.join(testdir, "line_length2")): bin
+ {
+ env.set env.str(str(config["var"]), "2147483647")
+ {
+ testdir: str = path.dirname(tgt_name);
+
+ other_tests: []str = config_list["other_tests"];
+
+ res := $ %(config_list["args"]) << @(other_tests[3]);
+
+ check_test(tgt_name, res,
+ path.join(testdir, OTHER_LINE_LEN70_RESULTS_NAME));
+ }
+ }
+
+ test sym(path.join(testdir, "expr_and_file_args_test")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ input_file: str = path.join(src_testdir, "add.txt");
+ input: str = io.read_file(input_file);
+ results_file: str = path.join(src_testdir, "add_results.txt");
+ results: str = io.read_file(results_file);
+
+ output_file: str = path.join(testdir, "expr_file_args.txt");
+
+ io.open(output_file, "w"): f
+ {
+ f.print(results +~ results +~ results +~ results);
+ }
+
+ res := $ %(config_list["args"]) -e $input -f $input_file
+ --expression $input --file $input_file
+ -e @(str(config["halt"]));
+
+ check_test(tgt_name, res, output_file);
+ }
+
+ test sym(path.join(testdir, "files_test")): bin
+ {
+ env.set env.str(str(config["var"]), "2147483647")
+ {
+ testdir: str = path.dirname(tgt_name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ input_file: str = path.join(src_testdir, "add.txt");
+ input: str = io.read_file(input_file);
+ results_file: str = path.join(src_testdir, "add_results.txt");
+ results: str = io.read_file(results_file);
+
+ output_file: str = path.join(testdir, "files.txt");
+
+ io.open(output_file, "w"): f
+ {
+ f.print(results +~ results +~ results +~ results);
+ }
+
+ res := $ %(config_list["args"]) -- $input_file $input_file
+ $input_file $input_file << @(str(config["halt"]));
+
+ check_test(tgt_name, res, output_file);
+ }
+ }
+
+ test sym(path.join(testdir, "line_length3")): bin
+ {
+ env.set env.str(str(config["var"]), "62")
+ {
+ testdir: str = path.dirname(tgt_name);
+
+ other_tests: []str = config_list["other_tests"];
+
+ res := $ %(config_list["args"]) -L << @(other_tests[3]);
+
+ check_test(tgt_name, res,
+ path.join(testdir, OTHER_LINE_LEN_RESULTS_NAME));
+ }
+ }
+
+ test sym(path.join(testdir, "line_length_func")): bin
+ {
+ env.set env.str(str(config["var"]), "62")
+ {
+ testdir: str = path.dirname(tgt_name);
+ results: str = tgt_name +~ ".txt";
+
+ path.mkdirp(testdir);
+
+ io.open(results, "w"): f
+ {
+ f.print("0\n");
+ }
+
+ other_tests: []str = config_list["other_tests"];
+
+ res := $ %(config_list["args"]) -L << @(other_tests[2]);
+
+ check_test(tgt_name, res, results);
+ }
+ }
+
+ test sym(path.join(testdir, "arg")): bin
+ {
+ halt: str = str(config["halt"]);
+
+ res1 := $ %(config_list["args"]) -h << $halt;
+ check_test_retcode(tgt_name, res1.exitcode);
+
+ res2 := $ %(config_list["args"]) -P << $halt;
+ check_test_retcode(tgt_name, res2.exitcode);
+
+ res3 := $ %(config_list["args"]) -R << $halt;
+ check_test_retcode(tgt_name, res3.exitcode);
+
+ res4 := $ %(config_list["args"]) -v << $halt;
+ check_test_retcode(tgt_name, res4.exitcode);
+
+ res5 := $ %(config_list["args"]) -V << $halt;
+ check_test_retcode(tgt_name, res5.exitcode);
+ }
+
+ test sym(path.join(testdir, "leading_zero_arg")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ calc: str = path.basename(testdir);
+
+ expected_file: str = tgt_name +~ ".txt";
+
+ expected: str = "0.1\n-0.1\n1.1\n-1.1\n0.1\n-0.1\n";
+
+ io.open(expected_file, "w"): f
+ {
+ f.print(expected);
+ }
+
+ data: str =
+ if calc == "bc"
+ {
+ "0.1\n-0.1\n1.1\n-1.1\n.1\n-.1\n";
+ }
+ else
+ {
+ "0.1pR\n_0.1pR\n1.1pR\n_1.1pR\n.1pR\n_.1pR\n";
+ };
+
+ res := $ %(config_list["args"]) -z << $data;
+
+ check_test(tgt_name, res, expected_file);
+ }
+
+ test sym(path.join(testdir, "invalid_file_arg")): bin
+ {
+ res := $ %(config_list["args"]) -f
+ "astoheusanotehynstahonsetihaotsnuhynstahoaoetusha.txt";
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "invalid_option_arg")): bin
+ {
+ other_tests: []str = config_list["other_tests"];
+
+ res := $ %(config_list["args"]) @("-" +~ other_tests[0])
+ -e @(str(config["halt"]));
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "invalid_long_option_arg")): bin
+ {
+ other_tests: []str = config_list["other_tests"];
+
+ res := $ %(config_list["args"]) @("--" +~ other_tests[1])
+ -e @(str(config["halt"]));
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "unrecognized_option_arg")): bin
+ {
+ res := $ %(config_list["args"]) -u -e @(str(config["halt"]));
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "unrecognized_long_option_arg")): bin
+ {
+ res := $ %(config_list["args"]) --uniform -e @(str(config["halt"]));
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "no_required_arg_for_option")): bin
+ {
+ res := $ %(config_list["args"]) -f;
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "no_required_arg_for_long_option")): bin
+ {
+ res := $ %(config_list["args"]) --file;
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "given_arg_for_long_option_with_no_arg")): bin
+ {
+ res := $ %(config_list["args"]) --version=5;
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "colon_option")): bin
+ {
+ res := $ %(config_list["args"]) -:;
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "colon_long_option")): bin
+ {
+ res := $ %(config_list["args"]) --:;
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "builtin_variable_arg_test")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ calc: str = path.basename(testdir);
+
+ extra: bool = bool(config["extra_math"]);
+
+ output: str =
+ if extra
+ {
+ "14\n15\n16\n17.25\n";
+ }
+ else
+ {
+ "14\n15\n16\n";
+ };
+
+ output_file: str = tgt_name +~ ".txt";
+
+ io.open(output_file, "w"): f
+ {
+ f.print(output);
+ }
+
+ data: str =
+ if extra
+ {
+ if calc == "bc"
+ {
+ "s=scale;i=ibase;o=obase;t=seed@2;ibase=A;obase=A;s;i;o;t;";
+ }
+ else
+ {
+ "J2@OIKAiAopRpRpRpR";
+ }
+ }
+ else
+ {
+ if calc == "bc"
+ {
+ "s=scale;i=ibase;o=obase;ibase=A;obase=A;s;i;o;";
+ }
+ else
+ {
+ "OIKAiAopRpRpR";
+ }
+ };
+
+ args: []str =
+ if extra
+ {
+ @[ "-S14", "-I15", "-O16", "-E17.25" ];
+ }
+ else
+ {
+ @[ "-S14", "-I15", "-O16" ];
+ };
+
+ res1 := $ %(config_list["args"]) %(args) << $data;
+ check_test(tgt_name, res1, output_file);
+
+ long_args: []str =
+ if extra
+ {
+ @[ "--scale=14", "--ibase=15", "--obase=16", "--seed=17.25" ];
+ }
+ else
+ {
+ @[ "--scale=14", "--ibase=15", "--obase=16" ];
+ };
+
+ res2 := $ %(config_list["args"]) %(long_args) << $data;
+ check_test(tgt_name, res2, output_file);
+ }
+
+ if calc == "bc"
+ {
+ io.open(path.join(testdir, OTHER_MATHLIB_SCALE_RESULTS_NAME), "w"): f
+ {
+ f.print("100\n");
+ }
+
+ test sym(path.join(testdir, "builtin_var_arg_with_lib")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ results_file: str = path.join(testdir,
+ OTHER_MATHLIB_SCALE_RESULTS_NAME);
+
+ res := $ %(config_list["args"]) -S100 -l << @("scale\n");
+
+ check_test(tgt_name, res, results_file);
+ }
+
+ test sym(path.join(testdir, "builtin_variable_long_arg_with_lib")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ results_file: str = path.join(testdir,
+ OTHER_MATHLIB_SCALE_RESULTS_NAME);
+
+ res := $ %(config_list["args"]) --scale=100 --mathlib <<
+ @("scale\n");
+
+ check_test(tgt_name, res, results_file);
+ }
+
+ test sym(path.join(testdir, "builtin_var_arg_with_lib_env_arg")): bin
+ {
+ env.set env.str("BC_ENV_ARGS", "-l")
+ {
+ testdir: str = path.dirname(tgt_name);
+ results_file: str = path.join(testdir,
+ OTHER_MATHLIB_SCALE_RESULTS_NAME);
+
+ res := $ %(config_list["args"]) -S100 << @("scale\n");
+
+ check_test(tgt_name, res, results_file);
+ }
+ }
+
+ test sym(path.join(testdir,
+ "builtin_var_long_arg_with_lib_env_arg")): bin
+ {
+ env.set env.str("BC_ENV_ARGS", "-l")
+ {
+ testdir: str = path.dirname(tgt_name);
+ results_file: str = path.join(testdir,
+ OTHER_MATHLIB_SCALE_RESULTS_NAME);
+
+ res := $ %(config_list["args"]) --scale=100 << @("scale\n");
+
+ check_test(tgt_name, res, results_file);
+ }
+ }
+
+ test sym(path.join(testdir, "builtin_var_env_arg_with_lib_arg")): bin
+ {
+ env.set env.str("BC_ENV_ARGS", "-S100")
+ {
+ testdir: str = path.dirname(tgt_name);
+ results_file: str = path.join(testdir,
+ OTHER_MATHLIB_SCALE_RESULTS_NAME);
+
+ res := $ %(config_list["args"]) -l << @("scale\n");
+
+ check_test(tgt_name, res, results_file);
+ }
+ }
+
+ test sym(path.join(testdir,
+ "builtin_var_long_env_arg_with_lib_arg")): bin
+ {
+ env.set env.str("BC_ENV_ARGS", "--scale=100")
+ {
+ testdir: str = path.dirname(tgt_name);
+ results_file: str = path.join(testdir,
+ OTHER_MATHLIB_SCALE_RESULTS_NAME);
+
+ res := $ %(config_list["args"]) -l << @("scale\n");
+
+ check_test(tgt_name, res, results_file);
+ }
+ }
+
+ test sym(path.join(testdir, "limits")): bin
+ {
+ res := $ %(config_list["args"]) << @("limits\n");
+
+ check_test_retcode(tgt_name, res.exitcode);
+
+ if str(res.stdout) == "" || str(res.stdout) == "\n"
+ {
+ error("Test \"" +~ tgt_name +~ "\" did not produce output");
+ }
+ }
+ }
+
+ test sym(path.join(testdir, "bad_arg_for_builtin_var_option")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+ calc: str = path.basename(testdir);
+
+ scale: str = if calc == "bc" { "scale\n"; } else { "K\n"; };
+
+ res1 := $ %(config_list["args"]) --scale=18923c.rlg << $scale;
+
+ check_err_test(tgt_name, res1);
+
+ if bool(config["extra_math"])
+ {
+ seed: str = if calc == "bc" { "seed\n"; } else { "J\n"; };
+
+ res2 := $ %(config_list["args"]) --seed=18923c.rlg << $seed;
+
+ check_err_test(tgt_name, res2);
+ }
+ }
+
+ test sym(path.join(testdir, "directory_as_file")): bin
+ {
+ testdir: str = path.dirname(tgt_name);
+
+ res := $ %(config_list["args"]) $testdir;
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "binary_file")): bin
+ {
+ res := $ %(config_list["args"]) @(file_dep);
+
+ check_err_test(tgt_name, res);
+ }
+
+ test sym(path.join(testdir, "binary_stdin")): bin
+ {
+ res := $ %(config_list["args"]) < @(file_dep);
+
+ check_err_test(tgt_name, res);
+ }
+}
+
+fn register_timeconst_tests(
+ bin: str,
+ testdir: str,
+ src_testdir: str,
+) -> void
+{
+ timeconst: str = path.join(testdir, "scripts/timeconst.bc");
+
+ if !path.isfile(path.join(src_dir, timeconst))
+ {
+ io.eprint("Warning: " +~ timeconst +~ " does not exist\n");
+ io.eprint(timeconst +~ " is not part of this bc because of " +~
+ "license incompatibility\n");
+ io.eprint("To test it, get it from the Linux kernel at " +~
+ "`kernel/time/timeconst.bc`\n");
+ io.eprint("Skipping...\n");
+
+ return;
+ }
+
+ for i: range(1001)
+ {
+ test sym(path.join(timeconst, str(i)))
+ {
+ idx: str = path.basename(tgt_name) +~ "\n";
+ file: str = path.join(src_dir, path.dirname(tgt_name));
+
+ // Generate.
+ res1 := $ bc -q $file << $idx;
+
+ if res1.exitcode != 0
+ {
+ io.eprint("Other bc is not GNU compatible. Skipping...\n");
+ return;
+ }
+
+ // Run.
+ res2 := $ %(config_list["args"]) -q $file << $idx;
+
+ if res2.exitcode != 0 || res2.stdout != res1.stdout
+ {
+ error("\nFailed on input: " +~ idx +~ "\n");
+ }
+ }
+ }
+}
+
+fn register_history_tests(
+ bin: str,
+ testdir: str,
+ src_testdir: str,
+) -> void
+{
+ calc: str = path.basename(testdir);
+
+ src_test_scriptdir: str = path.dirname(src_testdir);
+
+ len_res := $ @(path.join(src_test_scriptdir, "history.py")) $calc -a;
+
+ if len_res.exitcode != 0
+ {
+ io.eprint("Python 3 with pexpect doesn't work. Skipping history tests");
+ return;
+ }
+
+ len: usize = usize(str(len_res.stdout));
+
+ for i: range(len)
+ {
+ test sym(calc +~ "/history/" +~ str(i)): bin
+ {
+ name: str = tgt_name;
+ parts: []str = name.split("/");
+
+ calc: str = parts[0];
+ idx: str= parts[2];
+
+ src_testdir: str = path.join(src_dir, "tests");
+
+ $ @(path.join(src_testdir, "history.py")) -t $calc $idx @(file_dep);
+ }
+ }
+}
+
+/**
+ * Generates all of the test targets for an executable.
+ * @param name The base name of the executable.
+ * @param targets The targets that tests should depend on.
+ */
+fn exe_tests(name: str) -> void
+{
+ bin: str = exe_name(name);
+
+ testdir: str = path.join("tests", name);
+ src_testdir: str = path.join(src_dir, testdir);
+
+ halt: str = if name == "bc" { "halt"; } else { "q"; };
+ gen_options: []str = if name == "bc" { @[ "-lq" ]; };
+ options: []str = if name == "bc" { @[ "-lqc" ]; } else { @[ "-xc" ]; };
+
+ other_num: str = "10000000000000000000000000000000000000000000000000" +~
+ "0000000000000000000000000000";
+ other_num70: str = "10000000000000000000000000000000000000000000000" +~
+ "000000000000000000000\\\n0000000000";
+
+ other_tests: []str =
+ if name == "bc"
+ {
+ @[ "x", "extended-register", "line_length()", other_num ];
+ }
+ else
+ {
+ @[ "l", "mathlib", "glpR", other_num +~ "pR" ];
+ };
+
+ other_tests_results: []str = @[ other_num, other_num70 ];
+
+ var: str = name.toupper() +~ "_LINE_LENGTH";
+
+ script_options: []str =
+ if name == "bc"
+ {
+ @[ "-lqC" ];
+ }
+ else
+ {
+ @[ "-xC" ];
+ };
+
+ error_options: []str = if name == "bc" { @[ "-ls" ]; } else { @[ "-x" ]; };
+
+ args: []str =
+ if bool(config["valgrind"])
+ {
+ VALGRIND_ARGS +~ @[ "./" +~ bin ];
+ }
+ else
+ {
+ @[ "./" +~ bin ];
+ };
+
+ test_config: Gaml = @(gaml){
+ args: $args
+ halt: $halt
+ gen_options: $gen_options
+ options: $options
+ script_options: $script_options
+ error_options: $error_options
+ other_tests: $other_tests
+ other_tests_results: $other_tests_results
+ var: $var
+ };
+
+ push test_config: config_stack
+ {
+ extra: bool = bool(config["extra_math"]);
+
+ register_standard_tests(bin, testdir, src_testdir, extra);
+ register_script_tests(bin, testdir, src_testdir, extra);
+ register_stdin_tests(bin, testdir, src_testdir);
+ register_read_tests(bin, testdir, src_testdir);
+ register_error_tests(bin, testdir, src_testdir);
+ register_other_tests(bin, testdir, src_testdir, extra);
+
+ if name == "bc" && bool(config["generated_tests"]) &&
+ path.isfile(path.join(src_testdir, "scripts/timeconst.bc"))
+ {
+ register_timeconst_tests(bin, testdir, src_testdir);
+ }
+
+ if host.os != "Windows" && sym(config["history"]) == @builtin
+ {
+ register_history_tests(bin, testdir, src_testdir);
+ }
+ }
+}
+
+/**
+ * Gets the `$BINDIR`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$BINDIR`, with the `$DESTDIR`.
+ */
+fn get_bindir() -> str
+{
+ temp: str = str(config["bindir"]);
+
+ bindir: str =
+ if temp == ""
+ {
+ path.join(str(config["prefix"]), "bin");
+ }
+ else
+ {
+ temp;
+ };
+
+ return path.join(DESTDIR, bindir);
+}
+
+/**
+ * Gets the `$LIBDIR`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$LIBDIR`, with the `$DESTDIR`.
+ */
+fn get_libdir() -> str
+{
+ temp: str = str(config["libdir"]);
+
+ libdir: str =
+ if temp == ""
+ {
+ path.join(str(config["prefix"]), "lib");
+ }
+ else
+ {
+ temp;
+ };
+
+ return path.join(DESTDIR, libdir);
+}
+
+/**
+ * Gets the `$INCLUDEDIR`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$INCLUDEDIR`, with the `$DESTDIR`.
+ */
+fn get_includedir() -> str
+{
+ temp: str = str(config["includedir"]);
+
+ includedir: str =
+ if temp == ""
+ {
+ path.join(str(config["prefix"]), "include");
+ }
+ else
+ {
+ temp;
+ };
+
+ return path.join(DESTDIR, includedir);
+}
+
+/**
+ * Gets the `$PC_PATH`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$PC_PATH`, with the `$DESTDIR`.
+ */
+fn get_pc_path() -> str
+{
+ pc_path: str =
+ if str(config["pc_path"]) == ""
+ {
+ res := $ pkg-config --variable=pc_path pkg-config;
+
+ str(res.stdout);
+ }
+ else
+ {
+ str(config["pc_path"]);
+ };
+
+ return path.join(DESTDIR, pc_path);
+}
+
+/**
+ * Gets the `$DATAROOTDIR`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$DATAROOTDIR`, with the `$DESTDIR`.
+ */
+fn get_datarootdir() -> str
+{
+ temp: str = str(config["datarootdir"]);
+
+ datarootdir: str =
+ if temp == ""
+ {
+ path.join(str(config["prefix"]), "share");
+ }
+ else
+ {
+ temp;
+ };
+
+ return path.join(DESTDIR, datarootdir);
+}
+
+/**
+ * Gets the `$DATADIR`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$DATADIR`, with the `$DESTDIR`.
+ */
+fn get_datadir() -> str
+{
+ temp: str = str(config["datadir"]);
+
+ datadir: str =
+ if temp == ""
+ {
+ get_datarootdir();
+ }
+ else
+ {
+ temp;
+ };
+
+ return path.join(DESTDIR, datadir);
+}
+
+/**
+ * Gets the `$MANDIR`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$MANDIR`, with the `$DESTDIR`.
+ */
+fn get_mandir() -> str
+{
+ temp: str = str(config["mandir"]);
+
+ mandir: str =
+ if temp == ""
+ {
+ path.join(get_datadir(), "man");
+ }
+ else
+ {
+ temp;
+ };
+
+ return path.join(DESTDIR, mandir);
+}
+
+/**
+ * Gets the `$MAN1DIR`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$MAN1DIR`, with the `$DESTDIR`.
+ */
+fn get_man1dir() -> str
+{
+ temp: str = str(config["man1dir"]);
+
+ man1dir: str =
+ if temp == ""
+ {
+ path.join(get_mandir(), "man1");
+ }
+ else
+ {
+ temp;
+ };
+
+ return path.join(DESTDIR, man1dir);
+}
+
+/**
+ * Gets the `$MAN3DIR`, including the `$DESTDIR`. This generates the default
+ * value if it wasn't set.
+ * @return The `$MAN3DIR`, with the `$DESTDIR`.
+ */
+fn get_man3dir() -> str
+{
+ temp: str = str(config["man3dir"]);
+
+ man3dir: str =
+ if temp == ""
+ {
+ path.join(get_mandir(), "man3");
+ }
+ else
+ {
+ temp;
+ };
+
+ return path.join(DESTDIR, man3dir);
+}