31. Pike Test Suite
The test suite is used to check the compiler, interpreter and standard modules. It detects regressions, exercises error handling and boundary cases, and helps find problems that depend on the operating system, available libraries or build configuration. Tests should be added when introducing a feature or fixing a bug, and should typically be kept near the code they exercise.
Most tests are written in testsuite.in files in the
src/ and lib/ trees. These files are processed
with an m4 script bin/mktestsuite that expands their macros
into files named testsuite, which are then executed one test
at a time by Tools.Standalone.test_pike. Larger tests may
instead be standalone Pike scripts with a .test suffix.
Such files are useful for tests involving callbacks, processes
or an event loop.
31.1. Running Tests
From the source tree's top directory, the usual command is:
make verify
This generates the test suites, builds a test installation and runs it. Use BUILDDIR to select a particular build configuration when needed. The principal test-related make targets are:
- testsuites
- Generate tests from testsuite.in files, including the compiler and interpreter suite, native modules and Pike modules.
- tinstall
- Build a test installation in the build directory, replacing the previous test installation.
- verify
- Generate suites, make the test installation and run its tests. This also exercises the installation layout.
- just_verify
- Generate suites and run the Pike in the build tree without making a new test installation.
- verify_installed
- Generate suites and test the Pike installed in the configured installation directory instead of the build-tree interpreter.
- check, sure
- Aliases for verify.
- verbose_verify
- Run the build-tree tests with additional verbosity.
- gdb_verify
- Run the build-tree test runner under gdb.
- valgrind_verify, valgrind_just_verify
- Run the test installation or build-tree interpreter under Valgrind. Use VALGRINDARGS for Valgrind options.
TESTARGS supplies arguments to the
Tools.Standalone.test_pike test runner, not to the
installation program. It replaces the default arguments rather than
adding to them. The defaults normally run each suite in a separate
process and discover suites in the build tree and script tests in
the source trees. Include the discovery options or explicit suite
paths you need when overriding them. For example, to run only the core
suite with detailed diagnostics:
make just_verify TESTARGS="-v3 -f testsuite"
The make recipes run the test suite from the build directory, so relative paths in TESTARGS are relative to that directory. Generated native-module suites are under modules/ and post_modules/; suites from lib/ are under tlib/.
31.1.1. Running an individual suite
pike -x test_pike runs tests using the interpreter that executes the command. To test an uninstalled build, use the bin/pike script, which selects its master and module paths. Replace arch below with the actual build-directory name:
make testsuites bin/pike -x test_pike -v3 build/arch/testsuite pike -x test_pike -v3 lib/modules/Protocols.pmod/WebSocket.test
The last command tests the installed Pike, including its modules. Generated macro suites must have the basename testsuite; the runner does not accept arbitrary names for that format. bin/test_pike.pike is a compatibility entry point for the standalone tool. Use pike -x test_pike --help to inspect the options provided by the interpreter being tested.
31.1.2. Runner options
- -a, --auto[=directory]
- Discover files named testsuite and files ending in .test recursively. The default directory to search is the current directory. Repeat the option to search several trees. Presence of the file no_testsuites excludes its containing directory and its descendants.
- -v[level], --verbose[=level]
- Increase verbosity. Level 1 shows progress, level 2 identifies cases and skipped conditions, level 3 prints test source, level 4 also reports timings, and level 10 prints the final source including the runner's wrappers. Repeated verbosity options add their levels.
- -sN, --start-test=N
- Start at the one-based test number N in each suite. Earlier setup tests are not executed.
- -eN, --end-after=N
- Stop after the completed-test counter exceeds N. The current runner checks after execution, so -e0 runs one case and -e1 can run two.
- -f, --fail
- Stop after the first failed test.
- -F, --forked
- Run each suite in a separate subprocess, isolating its interpreter state from other suites.
- -lN, --loop=N
- Repeat the suites N times; the default is one pass.
- -p, --prompt
- Ask before executing each test.
- -tN, --trace=N
- Enable Pike tracing at the requested level.
- -c[level], --check[=level]
- Enable interpreter consistency checks when supported by the build. Positive levels add checks before tests; higher levels add checks around compilation and execution. Level 4 also forces garbage collection before tests. A negative level checks periodically instead.
- -m, --mem, --memory
- Report memory allocations after the tests.
- -S, --omit-slow-tests
- This causes the symbol OMIT_SLOW_TESTS to be #defined, allowing tests that take time to detect slow machines and reduce work-load.
- --failed-cond, --failed-conditionals
- Print conditions that caused tests to be skipped.
- -T, --notty
- Use output suitable for a log rather than an interactive terminal.
- --subproc-start=command
- Prefix subprocess commands, for example when running child interpreters under Valgrind.
- --no-watchdog
- Disable timeout monitoring. Normally the watchdog interrupts an unresponsive test after ten minutes, or forty minutes in a dmalloc build. Disable it only when needed for investigation, such as when running in a debugger.
- --watchdog=pid
- Run the watchdog process for a specified process. This option is normally used internally by the runner.
- -d, --debug
- Open a debug port when supported by the build.
- -h, --help
- List the runner's available options.
bin/pike -x test_pike -v3 -f -s12 -e0 build/arch/testsuite bin/pike -x test_pike -F --auto=lib/modules
A nonzero exit status indicates failure. Failed tests typically output the failed test's testsuite filename and line number and the actual and expected values. The summary also reports skipped tests; these are tests for which the preconditionals failed (like eg missing a module or the wrong OS).
31.2. Writing New Tests
Put a small, self-contained regression test in the suite nearest the affected code or in a testsuite that already covers that subsystem. A test suite should aim to cover common inputs, defaults, empty values, limits, invalid arguments and cleanup after errors. Use deterministic inputs and local resources; avoid depending on an external server or exact wall-clock timing.
31.2.1. A complete testsuite.in example
A macro suite is delimited by START_MARKER and END_MARKER. Use m4 quoting, [[ and ]], around code containing commas or other macro syntax. For example, save the following as /tmp/pike-example/testsuite.in:
START_MARKER
test_eq(sizeof("Pike"), 4)
test_equal([[reverse(({1, 2, 3}))]], [[({3, 2, 1})]])
test_any([[
array(int) values = ({1, 2, 3});
return sizeof(values);
]], 3)
test_eval_error([[error("Expected failure.\n");]])
END_MARKER
From the source tree's top directory, generate and run it with:
bin/mktestsuite /tmp/pike-example/testsuite.in > /tmp/pike-example/testsuite pike -x test_pike -v3 /tmp/pike-example/testsuite
Normal in-tree tests are generated by make testsuites.
A generated case contains wrapper functions, usually a()
for the operation and b() for its expected result. Each
case is compiled separately. Local variables and declarations in
one case therefore do not establish local state for another.
If state needs to be passed to later tests add_constant()
may be used. Note that such symbols must be removed by the time
the test suite completes. The expected expression is evaluated
too, so keep it simple and independent of the operation being tested.
31.2.2. test_eq and test_any
test_eq(expression, expected) compares the two results
with ==. test_any(code, expected) performs
the same comparison, but its first argument is a function body
that must return the result. Use it when setup or several
statements are needed. For separate arrays, mappings or multisets,
use the deep-comparison variants instead.
test_eq(1e1, 10.0) test_any([[ string text = "hello"; return upper_case(text); ]], "HELLO")
31.2.3. test_equal and test_any_equal
These variants compare results with equal(), which
compares container contents recursively. test_equal
takes expressions; test_any_equal takes a function body
followed by an expected expression. Equal contents do not imply
that two containers have the same identity.
test_equal([[({1, 2}) + ({3})]], [[({1, 2, 3})]])
test_any_equal([[
array(int) values = ({1, 2, 3});
return reverse(values);
]], [[({3, 2, 1})]])
31.2.4. test_approx and test_any_approx
These macros compare floating-point results using a tolerance derived from the interpreter's floating-point precision. test_approx takes expressions and test_any_approx takes a function body and an expected expression. Prefer them to exact equality when rounding differences are expected. They do not accept a caller-specified tolerance; use an explicit bounds check when the algorithm requires one.
test_approx(0.1 + 0.2, 0.3) test_any_approx([[return 0.1 + 0.2;]], 0.3)
31.2.5. test_true, test_false and test_do
test_true(expression) requires a true result and test_false(expression) requires a false result, using Pike's normal truth rules. test_do(code) compiles and executes statements; it fails on compilation errors, unexpected warnings or an uncaught exception. It does not compare a return value. Use an assertion inside the body when necessary.
test_true(has_prefix("Pike", "Pi"))
test_false(has_prefix("Pike", "Py"))
test_do([[
object file = Stdio.File();
destruct(file);
]])
31.2.6. test_compile and test_compile_any
test_compile(code) wraps statements in a function and verifies that they compile without errors or warnings. It does not execute them. test_compile_any(program) compiles a complete program without adding a function wrapper; use it for top-level declarations, inheritance and classes.
test_compile([[Stdio.File file = Stdio.File();]])
test_compile_any([[
class Counter {
int value;
int increment() { return ++value; }
}
]])
31.2.7. test_compile_error and test_compile_error_any
These verify that compilation fails, rather than that execution throws an exception. test_compile_error wraps statements in a function. test_compile_error_any takes a complete program. Use an unambiguous error so an unrelated diagnostic does not accidentally satisfy the test.
test_compile_error([[int value = "not an integer";]]) test_compile_error_any([[ int value = "not an integer"; ]])
31.2.8. test_compile_warning
This wraps statements in a function and succeeds if compilation produces a warning without a compilation error. It does not require a particular warning text or run the resulting program. For example, a constant that is hidden by an inherited variable produces a warning:
test_compile_warning([[
class A { int value = 6; }
class B {
constant value = 5;
inherit A;
}
]])
Ordinary execution tests also reject unexpected warnings. If a known warning is incidental to another test, the affected cases should be wrapped with ignore_warning(message, tests). Avoid broad suppression that could hide new warnings. That helper suppresses matching warnings; it does not assert their presence.
31.2.9. test_eval_error
test_eval_error(code) expects an exception while compiling
or executing the supplied function body. For historical reasons,
a compilation error also satisfies this macro. To test a runtime
failure specifically, keep the body compilable and use dynamic
arguments where a static type check would otherwise reject the
call. The macro does not inspect the exception's contents; use
catch and a comparison test when those matter.
test_eval_error([[error("Expected failure.\n");]])
test_any([[
mixed failure = catch { error("Expected failure.\n"); };
return failure[0];
]], "Expected failure.\n")
31.2.10. test_define_program
test_define_program(name, body) creates a class from the supplied body and installs it as a global constant for later tests. It is useful when several cases need the same helper program. Remove the constant after use so it does not affect subsequent tests. A selected range must include the definition before cases that depend on it.
test_define_program(GuideCounter, [[
int value;
int increment() { return ++value; }
]])
test_any([[
object counter = GuideCounter();
counter->increment();
return counter->increment();
]], 2)
test_do(add_constant("GuideCounter"))
31.2.11. test_program
test_program(program) compiles a complete program,
instantiates it and calls its a() method. That method
must return a true value for the test to succeed. This permits
helper methods, variables, inheritance and lfuns in the same
test program. test_program_eq(program, expected) and
test_program_equal(program, expected) compare its result
with == and equal(), respectively.
test_program([[
int value;
int increment() { return ++value; }
int a() { return increment() == 1 && increment() == 2; }
]])
31.2.12. cond, cond_resolv and ifefun
cond(expression, tests) runs the enclosed cases only if its Pike expression is true. A false condition skips cases rather than failing them; an error in a condition is a test failure. Conditions are cached by the runner, so they should check stable capabilities rather than changing per-test state.
cond_resolv(name, tests) checks whether a module or other named value can be resolved. ifefun(name, tests) checks for a global built-in function or constant. These are runtime availability checks, not C preprocessor tests. Use cond_begin(expression) and cond_end when a condition spans a longer group of cases; always balance them.
cond([[sizeof("Pike") == 4]], [[
test_eq(lower_case("PIKE"), "pike")
]])
cond_resolv(Stdio, [[
test_true(programp(Stdio.File))
]])
ifefun(sizeof, [[
test_eq(sizeof(""), 0)
]])
Guard optional features with their actual availability conditions. Do not make a broken feature look optional by skipping on the result being tested. Use --failed-cond to check why cases were skipped.
31.2.13. Regression tests
A regression test uses the ordinary test macros; no special nonregression macro is provided by the current suite generator. Reduce the original failure to a small example and assert the intended result or error. Add a short comment recording the bug or issue and why the case is significant. Test the smallest relevant boundary on either side of the failure, and ensure the case does not pass merely because unrelated setup failed.
31.3. Standalone script tests
A script test has a .test suffix and begins a comment
containing the marker TEST: RUN-AS-PIKE-SCRIPT. The
runner starts it in a child interpreter using the interpreter
under test. A script may use its own assertion helpers and event
loop. It should report its result with
Tools.Testsuite.report_result(); and report skipped cases
separately from successful and failed assertions.
// -*- pike -*- TEST: RUN-AS-PIKE-SCRIPT
int main()
{
int succeeded;
int failed;
if (equal(reverse(({1, 2, 3})), ({3, 2, 1})))
succeeded++;
else {
failed++;
werror("reverse() returned an unexpected result.\n");
}
Tools.Testsuite.report_result(succeeded, failed);
return failed ? 1 : 0;
}
Save the above as example.test and run pike -x test_pike -v3 example.test. An uncaught exception or abnormal child-process termination is a failure. A script containing several assertions should report their individual counts rather than just whether main() completed.
A macro suite can invoke a script with
Tools.Testsuite.run_script() and use test_tests
with a complete program whose a() returns the resulting
array of success, failure and skip counts. Prefer a directly
discovered .test file when no macro-suite integration is
needed. See lib/modules/Protocols.pmod/WebSocket.test for
a larger script test.
31.4. Investigating and reporting failures
Re-run the smallest failing suite using the same Pike binary and module paths. Increase verbosity to inspect the generated code and select the failing test with -s and -e. If that test depends on earlier setup, include those cases too. Use -l to investigate failures that require repeated execution, and an interpreter with runtime debugging enabled when investigating consistency-check failures.
Distinguish an unavailable optional feature from a failed assertion, an unexpected warning, a compiler error, a crash or a watchdog timeout. Keep the complete diagnostics and record the Pike version, platform, build options and command used to reproduce the problem. Where relevant, compare debug and optimized builds or use gdb and Valgrind. After fixing the focused case, run the surrounding suite and make verify to check for interactions.