π‘ The problem
Welcome back, it’s been quite a long time since my last ramblings on the D Programming Language!
This post is born from a necessity. An old project of mine, the D Koans, was using an external library to simplify unit testing, which is more or less the core of the whole project. Unfortunately, the library started giving some deprecation warnings when compiled with recent D versions.
Since the D Language already has an internal unit testing framework, I thought it would be nice to remove the single dependency and rely only on the standard library. Initially, with some global search/replace, I managed to convert all the tests to unittest
blocks.
π« Stack traces are ugly
Running these tests presented another challenge. Using the standard unit testing directly would confront users with dense error messages, such as:
core.exception.AssertError@koans/alias_this.d(39): unittest failure
----------------
??:? _d_unittestp [0x4bb84d]
koans/alias_this.d:39 void koans.alias_this.__unittest_L34_C1() [0x48b731]
??:? void koans.alias_this.__modtest() [0x48b788]
??:? int core.runtime.runModuleUnitTests().__foreachbody_L603_C5(object.ModuleInfo*) [0x4ccdb2]
??:? int object.ModuleInfo.opApply(scope int delegate(object.ModuleInfo*)).__lambda_L2467_C13(immutable(object.ModuleInfo*)) [0x4b2867]
??:? int rt.minfo.moduleinfos_apply(scope int delegate(immutable(object.ModuleInfo*))).__foreachbody_L582_C5(ref rt.sections_elf_shared.DSO) [0x4c1dc7]
??:? int rt.sections_elf_shared.DSO.opApply(scope int delegate(ref rt.sections_elf_shared.DSO)) [0x4c2149]
??:? int rt.minfo.moduleinfos_apply(scope int delegate(immutable(object.ModuleInfo*))) [0x4c1d55]
??:? int object.ModuleInfo.opApply(scope int delegate(object.ModuleInfo*)) [0x4b2839]
??:? runModuleUnitTests [0x4ccbe7]
??:? void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int function(char[][])*).runAll() [0x4c00dc]
??:? void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int function(char[][])*).tryExec(scope void delegate()) [0x4c0069]
??:? _d_run_main2 [0x4bffd2]
??:? _d_run_main [0x4bfdbb]
/usr/include/dlang/dmd/core/internal/entrypoint.d:29 main [0x484a69]
??:? [0x7f94bda2b12d]
??:? __libc_start_main [0x7f94bda2b1f8]
<unknown dir>/<unknown file>:115 _start [0x484884]
core.exception.AssertError@koans/arrays.d(8): unittest failure
which is less than ideal for a newbie; also all the unit tests run in parallel so you’d get a wall of weird text.
π¦Έ Metaprogramming to the rescue
The solution is to collect all the unit tests and run them manually in a foreach
loop!
This leads to another problem: the project is composed of many modules, similar to progressive “exercises” that the student must complete to learn. How do we enumerate all the modules, in a somewhat defined order, and make sure the main program imports them, and ensure the main program can import and call their functions? Let me introduce metaprogramming :)
Since all the exercises are in a directory, it’s easy to group them in a single package module
$ tree
.
βββ dscanner.ini
βββ dub.json
βββ koans
βΒ Β βββ alias_this.d
βΒ Β βββ arrays.d
βΒ Β βββ associative_arrays.d
βΒ Β βββ basics.d
βΒ Β βββ bitwise_operators.d
βΒ Β βββ chars.d
βΒ Β βββ c_interop.d
βΒ Β βββ classes.d
βΒ Β βββ concurrency.d
βΒ Β βββ ctfe.d
βΒ Β βββ delegates.d
βΒ Β βββ enums.d
βΒ Β βββ exceptions.d
βΒ Β βββ files.d
βΒ Β βββ foreach_loop.d
βΒ Β βββ function_parameters.d
βΒ Β βββ helpers.d
βΒ Β βββ lambda_syntax.d
βΒ Β βββ mixins.d
βΒ Β βββ numbers.d
βΒ Β βββ operator_overloading.d
βΒ Β βββ package.d <--------------- THIS
βΒ Β βββ pointers.d
βΒ Β βββ properties.d
βΒ Β βββ strings.d
βΒ Β βββ structs.d
βΒ Β βββ templates.d
βΒ Β βββ traits.d
βΒ Β βββ tuples.d
βΒ Β βββ unions.d
βββ learn.d
βββ README.md
βββ scripts
βββ runner_linux.sh
βββ runner_osx.sh
our package.d
is simple:
|
|
instead of importing all the modules, we use a loop to create at compile time the import statements. In this way, the main program only needs to import koans
as a whole package.
note: we will reuse the same list of modules in the main
program:
βοΈ A custom test runner
|
|
The important parts are:
- line 9-14 : need to override the default Runtime.moduleUnitTester function. This will let our
main
run even when the program is compiled with--unittest
flag. - line 21: iterate on each module, reusing the same array of strings previously defined in
package.d
- line 23: build a scoped import statement with the module name, prefixed by package name (e.g.
koans.basics
) - line 24: use traits to iterate over all unit tests of that module, calling the unit test (which is wrapped as a function) inside a
try-catch
block in order to capture the AssertError - line 29: if the unit test fails, give the user instructions on which line of which file needs to change and the program terminates
(mandatory AI-generated catchy image)
β Conclusions
My project now does not depend on any other library, and it will be very simple to add new tests: just follow the language conventions and create a new file with unit tests, then write its name in the proper position of the array.
I hope this practical example of D’s capabilities was insightful. More importantly, has it made you curious to learn more about the D programming language itself?
Have you used D’s metaprogramming for similar tasks? Feedbacks are welcome!