/*
* *****************************************************************************
*
* 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);
}