Named unittests

Johannes Pfau nospam at example.com
Sun May 19 09:25:01 UTC 2019


Am Sat, 18 May 2019 13:55:39 -0400 schrieb Andrei Alexandrescu:

> 
> Wait, if you build a program with -unittest will it run some/all of
> phobos' unittests? That would be indeed undesirable!
> 
> I wonder how often people compile external libraries together with the
> application within the same command line.

It runs some of the phobos unittests, those which are in templates 
instantiated in user code:
-------------------------------------------------
module phobos;
struct Foo(T)
{
    unittest {import std.stdio; writeln("Test");}
}

module user;
import phobos;
void main(){Foo!int a;}
-------------------------------------------------
dmd -unittest user.d

If you remove the Foo template parameter, the test will no longer run. 
The reason for this behavior is simple: We can only run the test for 
template instances, not for declarations.

The compiler could easily check whether the template declaration of a 
unittest in a template instance is part of the root modules (modules 
present on DMD command line) and skip other tests. However, this will 
break the test patterns where people intentionally instantiate templates 
in other modules. Tests in the same module do not cover visibility issues 
and especially if we ever get proper DLL support for windows, where we 
would have to mark functions explicitly as export, you really do want to 
test in another module, likely even in another library.




I think the biggest problem with D's unittest implementation is that it 
isn't built on small, orthogonal and composable features. Instead we 
collect all unittests in a module, generate a function which calls them 
one by one and the add a pointer to that function to ModuleInfo. This 
implementation is highly specific and very inflexible: With the current 
framework, we simply cannot continue running tests after one failed, as 
the compiler essentially concatenated all of them into one function.

I posted a DMD PR which allows running tests individually seven/five 
years ago:
https://github.com/dlang/dmd/pull/1131 https://github.com/dlang/dmd/pull/
3518
Back then this was shot down on details whether we should allow to 
disable unittests and whether we should add file/line information. With 
the current implementation (storing information in ModuleInfo), this is 
something we as compiler developers have to decide on. But all these 
issues are detail decisions, which should really be left to unittest 
library/runner authors. For the core language/compiler we should rather 
provide reusable building blocks which are flexible enough to allow 
different library implementations instead of a highly specific, 
inflexible implementation.



So why do we have this specific compiler implementation? What does it do 
that library code can't do? Basically it does two things:

1) Test discovery: Decide which tests should be run and collect all of 
them. The selection criteria is essentially this: Unittests in files 
passed to dmd like this: 'dmd -unittest file.d' are selected. Also some 
tests in other modules because of the template problem. 

2) Test registration: For each discovered tests, make it somehow possible 
for the runtime to run it.


Now we do have library test runners*. They use compile time reflection to 
find tests, build on UDAs and other well-known language features and I 
really think this is the way to go. In order to switch druntime to such 
an implementation, we would need to reach feature parity with the current 
implementation though. The main problem here is in 1), as reflection 
based runners require you to somehow list all modules which should be 
tested.
I do not think we want to mess with ModuleInfo here, as this always 
involves serializing compile-time type information to runtime 
information. We want a completely compile time solution here. So what we 
need is some way for a library to reflect on every other application 
module automatically. I'd propose to leverage template mixins for this 
and extend them in one crucial point. Template mixins almost do what we 
want:


-------------------------------------------------
module test;

void registerTest(string name)
{
    import std.stdio;
    writeln(name);
}

mixin template registerMixin(string name)
{
    pragma(crt_constructor)
    extern(C) void registerUnittest()
    //FIXME: Need a unique mangle for this function
    {
        mixin("alias members = __traits(allMembers, " ~ name ~ ");");
        foreach(member; members)
            registerTest(member.stringof);
    }
}
-------------------------------------------------
module user;
import test;

mixin registerMixin!"user";

void main() {}
-------------------------------------------------
dmd main.d test.d


The only problem here is that we have to manually invoke `mixin 
registerMixin!"user";` in every module we want to test. Now what if we add 
`import mixins`? Simply change the `registerMixin` signature to `import 
mixin template registerMixin(string name)` and whenever the module is 
imported, the compiler automatically inserts the mixin line into the 
importing module. Essentially an import mixin template is a template 
automatically mixed in into importing modules and able to reflect on 
these modules. This small addition now allows us to use all the compile 
time, template based reflection stuff and have automatically registered 
tests. As this feature is generic we could also use it to auto-register 
@benchmark(UDA) functions. Or automatically generate serialization stuff. 
Or generate runtime typeinfo. Or .... And every library author can do the 
same.

I realize this feature is a bit controversial, as it essentially allows 
library authors to silently hook code into user modules. OTOH I think 
this is a very orthogonal feature enabling many new idioms to be explored.



2) is then easily solved, we already have crt_constructor. You could also 
place pointers into a named section for low level targets and there may 
be other options. Registration is basically a solved problem.



For the default druntime tester, we could just add such an import mixin 
template to object.d, so it's included in every file. Then wrap the code 
in the mixin template in version(unittest) and we're mostly done. Not 
including a main function / running the main application logic would 
likely be the users responsibility then, as a D library cannot really 
prevent the runtime from calling main. The remaining problem are tests in 
templates (see *), as we can't automatically register these without 
additional compiler help. For now, the compiler could explicity emit 
registerTest calls for these and we could add a deprecation for this, 
advising to explicitly test using RegisterTests!(T).


* Note that CTFE reflection based test runners do not run tests in 
templated aggregates, so they are not affected by the template issue. 
Actually, this seems to be a very good thing: You could easily create a 
template which runs nested tests on demand using reflection on the type:
RegisterTests!(Foo!int);
You can easily place this into different libraries, modules, files, ... 
and it will always work as expected.

-- 
Johannes


More information about the Digitalmars-d mailing list