7.1. Testing

7.1.1. Functionality Testing

The goal of the functionality testing is to check both typical features as well as edge cases. Not only should the code “run”, but also produce correct results. This is guaranteed by many automated test cases.

Some examples include:

  • a nearly ideal lens produces a distinct focal point at the correct focal distance

  • a growing number of rays decreases sampling noise

  • user-built surfaces modeling a spherical surface behaving the same as the in-built type

  • sampling of directions, color, positions produces the correct distribution on both the source and destination

  • GUI interactions execute the expected actions

  • images are correctly loaded and saved

  • typical phenomena and aberrations of geometrical optics can be reproduced (spherical and chromatic aberration, vignetting, dispersion, astigmatism, coma, …)

  • image formation by convolving produces correct results even the images must be scaled or interpolated

  • PSF convolution and raytracing produce comparable results

  • simple .zmx and .agf files are handled and imported correctly

  • … many more

Some edge cases include:

  • normalizing a black image does not lead to divisions by zero

  • requesting billions of rays prints an error message instead of crashing the PC due to too much requested RAM

  • collisions of lenses are detected

  • moving geometry elements outside of the setup bounding box is not possible

  • check of value validity (no negative powers, wavelengths outside the visible range, …)

  • plotting/loading/tracing of a single pixel image

  • an optical setup without lenses, filters or apertures is traced correctly

  • … many more

Test files can be found under tests/, starting with filename test_ Testing is performed with pytest. To ensure that it is performed on a “clean” and defined environment, it is run by tox, this creates a virtual python environment where the library is installed and tested. With its default call, tox calls the testing environment that includes all test cases. To lower RAM usage, tox calls test groups instead of all tests at once.

The workflow file tox_test.yml runs the functionality tests on a defined system and python version as github actions. By default this is done on pushed code changes, but the action can also be run manually.

7.1.2. Coverage Testing

coverage is used to check if all code lines and branches were tested. It wraps around pytest inside the tox test environment and reports on executed and missing lines. The output gets represented as both html or as text output (inside the terminal or github action output).

7.1.3. Benchmark Testing

Tracing performance depends on the number of surfaces and rays as well as the complexity of the surfaces. Spherical surfaces are easier to trace then complex user defined ones. For systems with a low number of surfaces the ray generation at the source becomes dominant.

The benchmarking file tests/benchmark.py is an adapted version of the Microscope example. It includes 57 light-interacting surfaces (one ray source, one aperture, 27 lenses with front and back surface each, one outline surface of the bounding box). Detectors are excluded as they don’t interact with the rays unless image rendering is executed. Turning off the polarization calculation (with Raytracer.no_pol) leads to a significant speedup.

The raytracing on my system (Arch Linux 6.13, i7-1360P notebook CPU, 16GB RAM, Python 3.13) results in:

Table 7.1 Performance Comparison. Result values are in seconds / surface / million rays.

Cores

Without Polarization

With Polarization

1

0.262

0.417

2

0.122

0.171

4

0.069

0.125

8

0.059

0.108

16

0.057

0.101

So there is not much gain in using 16 over 8 cores.

7.1.4. Test Cases

  • doctest for documentation strings in some non-class functions

  • test cases are handled with unittest, testing is however typically done by pytest that loads and executed the created TestSuites

  • TraceGUI has a debug method, which runs a separate thread, from which actions are executed:
    • Note that each UI actions needs to be run in the main thread, for this TraceGUI provides _do_in_main and _set_in_main methods

    • generally we want to do an action, after the old one has finished. For this TraceGUI._wait_for_idle is implemented.

7.1.5. Documentation Testing

The workflow file doc_test.yml runs multiple test environments. This includes:

  • tox -e docsbuildcheck: Testing of correct documentation building while not permitting warning

  • tox -e linkcheck: Testing of all documentation links

  • tox -e doctest: Testing of most documentation code examples (using doctest)

7.1.6. Python Version Testing

Testing of the compatibility with multiple python versions is done with the pyver_comp.yml workflow. It executes a subset of tests (tox -e fast) for multiple python main versions in an Ubuntu runner.

7.1.7. Installation Testing

Weekly tests that try to install the package and execute some quick tests. Ensures that requirements and available PyPI packages are up-to-date or need changes. The workflow is located in install_test.yml.

7.1.8. Platform Testing

Platform testing is done with the os_comp.yml workflow. It executes the os tox environment, which runs a subset of all available tests. These tests include relevant possibly platform-dependent cases, including:

  • installation

  • loading and saving of files

  • handling of filenames and paths

  • loading sensitivity and image presets (which also are files inside the library)

  • opening the gui and plots

  • enforcing a specific float bit width

  • detecting the number of cores and multithreading

The workflow is executed with the current python version and both the latest macOC and Windows runners. Linux is not included, as all other workflows already run on Ubuntu.

7.1.9. Github Workflows

  • see .github/workflows/

  • Actions: Main testing, OS Compatibility, Older Python Version Compatibility

  • all get run on a push the repository or can get run manually

7.1.10. Manual Tests

Unfortunately, not all tests can be automated. The following test cases need to be handled manually:

Checking that optrace.plots.block actually pauses the program execution

This can’t be tested automatically, as it would halt testing. Maybe there is a way by using multithreading or multiprocessing and a timer? What about coverage information in such cases?

Correct and nice formatting inside plots and the GUI

Can only be tested by a human viewer.

Usage in Python Notebooks or inline IDEs such as Spyder

  • Installation:
  • Testing:
    • make sure pyplot windows are displayed correctly (plot viewer) with enough dpi

    • make sure this is also the case for plots generated by the GUI

7.1.11. Tox configuration in tox.toml

 1env_list = ["py{310,311,312,313}"]
 2requires = ["tox-ignore-env-name-mismatch"]
 3skip_missing_interpreters = true
 4
 5[env_run_base]
 6description = "Main testing environment"
 7pass_env = ["HOME", "DISPLAY", "WAYLAND_DISPLAY", "QT_QPA_PLATFORM", "XAUTHORITY", "GITHUB_ACTIONS", "PYTHON_CPU_COUNT",
 8            "XDG_SESSION_TYPE", "XDG_SESSION_DESKTOP"]
 9env_dir = "{toxworkdir}{/}env_static"
10runner = "ignore_env_name_mismatch"
11deps = ["pylint"]
12extras = ["tests", "docs"]
13commands = [
14    ["coverage", "run", "-p", "--source=optrace", "--branch", "-m", 
15        "pytest", {replace="posargs", extend=true}, "tests{/}test_geometry.py", "tests{/}test_misc.py", 
16        "tests{/}test_image.py", "tests{/}test_refraction_index.py", "tests{/}test_scope.py", 
17        "tests{/}test_spectrum.py", "tests{/}test_surface.py", "tests{/}test_tma.py"
18    ],
19    ["coverage", "run", "-p", "--source=optrace", "--branch", "-m", 
20        "pytest", {replace="posargs", extend=true}, "tests{/}test_tracer.py", "tests{/}test_tracer_hurb.py", 
21        "tests{/}test_tracer_special.py"
22    ],
23    ["coverage", "run", "-p", "--source=optrace", "--branch", "-m", 
24        "pytest", {replace="posargs", extend=true}, "tests{/}test_color.py", "tests{/}test_convolve.py", 
25        "tests{/}test_load.py"
26    ],
27    ["coverage", "run", "-p", "--source=optrace", "--branch", "-m", 
28        "pytest", {replace="posargs", extend=true}, "tests{/}test_plots.py"
29    ],
30    ["coverage", "run", "-p", "--source=optrace", "--branch", "-m", 
31        "pytest", {replace="posargs", extend=true}, "tests{/}test_examples.py"
32    ],
33    ["coverage", "run", "-p", "--source=optrace", "--branch", "-m",
34        "pytest", {replace="posargs", extend=true}, "-m", "gui1", "tests{/}test_gui.py"
35    ],
36    ["coverage", "run", "-p", "--source=optrace", "--branch", "-m", 
37        "pytest", {replace="posargs", extend=true}, "-m", "gui2", "tests{/}test_gui.py"
38    ],
39    ["coverage", "run", "-p", "--source=optrace", "--branch", "-m", 
40        "pytest", {replace="posargs", extend=true}, "-m", "gui3", "tests{/}test_gui.py"
41    ],
42    ["coverage", "combine"],
43    ["coverage", "report", "--show-missing"],
44    ["coverage", "erase"]
45]
46
47[env.os]
48description = "Testing of system dependent functionality"
49commands = [["pytest", "-m", "os", {replace="posargs", extend=true}]]
50
51[env.fast]
52description = "Don't test slower tests (> 10 seconds)"
53commands = [
54    ["pytest", "-m", "not slow", "--ignore=tests{/}test_examples.py", "--ignore=tests{/}test_gui.py", 
55        {replace="posargs", extend=true}],
56    ["pytest", "-m", "not slow", {replace="posargs", extend=true}, "tests{/}test_examples.py"],
57    ["pytest", "-m", "not slow", {replace="posargs", extend=true}, "tests{/}test_gui.py"],
58]
59
60[env.install]
61description = "Quick testing of installation"
62commands = [["pytest", "-v", "-m", "install", {replace="posargs", extend=true}]]
63
64[env.linkcheck]
65description = "Check all links in software and documentation"
66commands = [["sphinx-build", "-M", "linkcheck", "docs{/}source", "docs{/}build", "-j", "auto"]]
67
68[env.docsbuildcheck]
69description = "Nitpicky documentation building, turn warnings into errors"
70commands = [["sphinx-build", "-M", "html", "docs{/}source", "docs{/}build{/}htmlcheck", "-n", "-W"]]
71
72[env.doctest]
73description = "Test documentation doctest snippets"
74commands = [["sphinx-build", "-M", "doctest", "docs{/}source", "docs{/}build", "-j", "auto"]]
75
76[env.docs]
77description = "Create documentation and update structure and changelog"
78allowlist_externals = ["{/}usr{/}bin{/}bash"]
79commands = [
80    ["bash", "docs{/}generate_changelog.sh"],
81    ["bash", "docs{/}generate_bib_structure.sh"],
82    ["sphinx-build", "-M", "html", "docs{/}source", "docs{/}build", "-n"]
83]
84
85[env.pylint]
86description = "Run pylint with excluded unnecessary rules"
87skip_install = true
88commands = [
89    ["pylint", "optrace", "-j", "0", "--max-line-length", "120", "--variable-naming-style", "any",
90    "--disable", "W0612,W0613,W0106,W0212,E1101,R0902,R0904,R0911,R0912,R0913,R0914,R0915,R0916,C0301,C0302",
91    "--good-names-rgxs", "^[_a-z][_a-z0-9]?$"]
92]

Notes

  • tox-ignore-env-name-mismatch allows us to reuse the tox env for all actions

  • parallelized testing with pytest-xdist could be possible, but we wouldn’t gain much from it as the heaviest tasks are already multithreaded

  • pytest-xvfb uses xvfb as headless-display. Use the option --no-xvfb to actually see the plots/windows.

  • when GUI tests fail on wayland, first run xhost +