Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ jobs:
sudo mv OpenSCAD-2021.01*-x86_64.AppImage /usr/local/bin/openscad
sudo chmod +x /usr/local/bin/openscad

- name: Install openscad-test
run: pip install openscad-test

- name: Run Regression Tests
run: |
cd $GITHUB_WORKSPACE
export OPENSCADPATH=$(dirname $GITHUB_WORKSPACE)
./scripts/run_tests.sh

CheckTutorials:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,4 @@ BOSL2_Docs.html
BOSL2_Docs.epub
BOSL2_Docs.pdf

tests/tmp*.scad
133 changes: 133 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Testing BOSL2

BOSL2 uses the [`openscad-test`](https://pypi.org/project/openscad-test/) framework for regression testing. Tests are defined in `tests/test_*.scadtest` files — one file per BOSL2 module.

## Requirements

- OpenSCAD 2021.01 or later
- Python 3.x with `openscad-test` installed:
```bash
pip install openscad-test
```

## Running Tests

Run all regression tests from the BOSL2 root directory:
```bash
./scripts/run_tests.sh
```

Run tests for a specific module:
```bash
openscad-test tests/test_transforms.scadtest
```

Run tests for multiple specific modules:
```bash
openscad-test tests/test_math.scadtest tests/test_lists.scadtest
```

## Writing Tests

Each `tests/test_<module>.scadtest` file contains one `[[test]]` entry per function or module being tested. Tests are written in TOML format using TOML literal multiline strings (`'''`) for the inline OpenSCAD script.

### Basic structure

```toml
[[test]]
name = "test_my_function"
script = '''
include <../std.scad>

module test_my_function() {
assert(my_function(1, 2) == 3);
assert_approx(my_function(1.5, 2.5), 4.0);
}
test_my_function();
'''
```

The `include <../std.scad>` path resolves relative to the `tests/` directory. For modules not included by `std.scad`, add the extra include:

```toml
[[test]]
name = "test_something"
script = '''
include <../std.scad>
include <../fnliterals.scad>

module test_something() {
assert(something() == expected);
}
test_something();
'''
```

### Test pass/fail rules

A test **passes** when OpenSCAD exits successfully and produces:
- No `ECHO` output (use `assert()` rather than `echo()`)
- No `WARNING` output

A test **fails** if OpenSCAD exits with an error, or produces any unexpected ECHO or WARNING output.

### Useful assertion helpers (from `std.scad`)

| Helper | Use |
|--------|-----|
| `assert(expr)` | Fails if `expr` is false |
| `assert(expr, msg)` | Fails with message |
| `assert_approx(got, expected)` | Approximate equality (floating point) |
| `assert_equal(got, expected)` | Exact equality with diagnostic output |

### Helper functions and modules

If the test module needs a helper defined in the same `.scadtest` file (e.g., a shared helper used by multiple tests), define it in the `script` before the test module:

```toml
[[test]]
name = "test_my_function"
script = '''
include <../std.scad>

function my_helper(x) = x * 2;

module test_my_function() {
assert(my_function(my_helper(3)) == 6);
}
test_my_function();
'''
```

### Advanced options

The `[[test]]` section supports these optional fields:

```toml
[[test]]
name = "test_name"
script = '''...'''
expect_success = true # default: true; set false to expect failure
assert_echoes = ["ECHO: 42"] # require specific ECHO output substrings
assert_no_echoes = true # default: true
assert_warnings = ["WARNING: foo"] # require specific WARNING substrings
assert_no_warnings = true # default: true
set_vars = {var = "value"} # pass -D variables to OpenSCAD
```

## Function Coverage

To check which public functions lack test coverage:

```bash
python3 scripts/func_coverage.py
```

This reports which functions in the library source files have no corresponding `test_<funcname>` entry in the `.scadtest` files.

## Test File Naming Convention

Each `tests/test_<module>.scadtest` file should test the functions and modules defined in `<module>.scad`. Each `[[test]]` entry name should match the function or module being tested:

- `name = "test_translate"` → tests `translate()` from `transforms.scad`
- `name = "test_path_length"` → tests `path_length()` from `paths.scad`
8 changes: 4 additions & 4 deletions fnliterals.scad
Original file line number Diff line number Diff line change
Expand Up @@ -1754,17 +1754,17 @@ function f_is_region(a) = f_1arg(function (a) is_region(a))(a);
function f_is_vnf(a) = f_1arg(function (a) is_vnf(a))(a);


// Function: f_is_patch()
// Function: f_is_bezier_patch()
// Synopsis: Returns a function to determine if a value is a Bezier Patch structure.
// Topics: Function Literals, Type Queries
// See Also: f_is_undef(), f_is_bool(), f_is_num(), f_is_int(), f_is_string(), f_is_list()
// Usage:
// fn = f_is_patch();
// fn = f_is_bezier_patch();
// Description:
// A factory that returns function literals equivalent to `is_patch(a)`.
// A factory that returns function literals equivalent to `is_bezier_patch(a)`.
// Arguments:
// a = If given, replaces the argument.
function f_is_patch(a) = f_1arg(function (a) is_patch(a))(a);
function f_is_bezier_patch(a) = f_1arg(function (a) is_bezier_patch(a))(a);



Expand Down
11 changes: 7 additions & 4 deletions scripts/func_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,19 @@
covered = {}
uncovered = funcs.copy()
for filename in os.listdir("tests"):
if filename.startswith("test_") and filename.endswith(".scad"):
if filename.startswith("test_") and filename.endswith(".scadtest"):
filepath = os.path.join("tests",filename)
with open(filepath, "r") as f:
for linenum,line in enumerate(f.readlines()):
if line.startswith("module "):
testmodule = line[7:].strip().split("(")[0].strip()
line = line.strip()
if line.startswith("name = \"test_") and line.endswith("\""):
testmodule = line[8:].strip('"')
if testmodule.startswith("test_"):
funcname = testmodule.split("_",1)[1]
if funcname in uncovered:
if filename != "test_" + uncovered[funcname][0]:
scadfile = "test_" + uncovered[funcname][0]
scadtestfile = scadfile.replace(".scad", ".scadtest")
if filename != scadtestfile:
print("WARNING!!! Function {} defined at {}:{}".format(funcname, *uncovered[funcname]));
print(" but tested at {}:{}".format(filename, linenum+1));
covered[funcname] = (filename,linenum+1)
Expand Down
37 changes: 2 additions & 35 deletions scripts/run_tests.sh
Original file line number Diff line number Diff line change
@@ -1,41 +1,8 @@
#!/bin/bash

OPENSCAD=openscad
if [ "$(uname -s)" == "Darwin" ]; then
OPENSCAD=/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD
fi

INFILES=("$@")
if (( ${#INFILES[@]} == 0 )); then
INFILES=(tests/test_*.scad)
INFILES=(tests/test_*.scadtest)
fi


cleanup () {
rm -f out.echo
exit
}

# clean up out.echo if we terminate due to a signal

trap cleanup SIGINT SIGHUP SIGQUIT SIGABRT

OUTCODE=0
for testfile in "${INFILES[@]}"; do
if [[ -f "$testfile" ]] ; then
repname="$(basename "$testfile" | sed 's/^test_//')"
"${OPENSCAD}" -o out.echo --hardwarnings --check-parameters true --check-parameter-ranges true "$testfile" 2>&1
retcode=$?
output=$(cat out.echo)
if (( retcode == 0 )) && [[ "$output" = "" ]]; then
echo "$repname: PASS"
else
echo "$repname: FAIL!"
echo "$output"
OUTCODE=1
fi
rm -f out.echo
fi
done
exit "$OUTCODE"

openscad-test "${INFILES[@]}"
Loading
Loading