diff options
Diffstat (limited to 'contrib/bc/build.pkg.rig')
| -rw-r--r-- | contrib/bc/build.pkg.rig | 2345 |
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); +} |
