即使一个测试失败,D也会执行模块中的所有测试

时间:2016-11-26 22:38:42

标签: unit-testing d

我正在尝试编写自己的moduleUnitTester,它将执行模块中的所有测试,即使一个失败也是如此。默认单位测试程序的工作方式类似于this

size_t failed = 0;
foreach (m; ModuleInfo) {
    if (m) {
        auto fp = m.unitTest;
        if (fp) {
            try {
                fp();
            }
            catch (Throwable e) {
                writeln(e);
                failed++;
            }
        }
    }
}

fp()抛出第一次失败。 我真的不喜欢这样,m.unitTest返回void函数,这是一个将执行模块中所有单元测试的函数。有没有办法列出这些单元测试并迭代每个单元测试?这不起作用:

 foreach (m; ModuleInfo)
 {
   __traits(getUnitTests, m);
 }

这将让我抓住所有的单元测试,然后自由地迭代它们。 说'm'是变量而不是模块。我找不到任何文档什么是'ModuleInfo'实际上我发现这只是错误...

2 个答案:

答案 0 :(得分:1)

好吧,既然我在评论中写了一半答案,我想我也可以在这里写一点。

这里有几个选项:编译时间和运行时间。编译时可以让您访问每个模块的UDA和其他详细信息,但是不能很好地访问所有模块。您可以尝试遍历导入图,但此方法无法使用本地导入。您可以使用构建工具列出您的模块,但这当然要求您实际使用具有该功能的构建工具(https://github.com/atilaneves/unit-threaded是一个使用dub执行此操作的库)。

您也可以将模块的手动列表传递给测试运行器,这是最大的维护工作,但可能具有很好的灵活性。

但是,我想在运行时这样做,就像你在问题中一样,只是更详细。怎么样?通过做一些低级指针的东西!有时仍然需要成为一名旧的汇编语言黑客:)

看看带有内联注释的代码:

module q.test;

unittest {
        assert(0, "Test 1 failed");
}

unittest {
        assert(true);
}

 // module ctor to set our custom test runner instead of the default
shared static this() {
        import core.runtime;
        Runtime.moduleUnitTester = &myhack;
}

bool myhack() {

/*

OK, so here's the situation. The compiler will take each unittest block
and turn it into a function, then generate a function that calls each
of these functions in turn.

core.runtime does not give us access to the individual blocks... but DOES
give us the unitTest property on each module compiled in (so we catch them
all automatically, even with separate compilation, unlike with the CT
reflection cases) which is a pointer to the auto-generated function-calling
function.

The machine code for this looks something like this:

0000000000000000 <_D1q4test9__modtestFZv>:
   0:   55                      push   rbp
   1:   48 8b ec                mov    rbp,rsp
   4:   e8 00 00 00 00          call   9 <_D1q4test9__modtestFZv+0x9>
   9:   e8 00 00 00 00          call   e <_D1q4test9__modtestFZv+0xe>
   e:   5d                      pop    rbp
   f:   c3                      ret


The push and mov are setting up a stack frame, irrelevant here. It is the
calls we want: they give us pointers to the individual functions. Let's dive in.

*/

        bool overallSuccess = true;

        foreach(mod; ModuleInfo) {
                // ModuleInfo is a runtime object that gives info about each
                // module. One of those is the unitTest property, a pointer
                // to the function described above.
                if(mod.unitTest) {
                        // we don't want a function, we want raw bytes!
                        // time to cast to void* and start machine code
                        // hacking.
                        void* code = mod.unitTest();
                        version(X86_64) {
                                code += 4; // skip function prolog, that push/mov stuff.
                        } else version(X86) {
                                code += 3; // a bit shorter on 32 bit
                        } else static assert(0);

                        // Opcode 0xe8 is the 32-bit relative call,
                        // as long as we see those calls, keep working.
                        while(* cast(ubyte*) code == 0xe8) {
                                code++; // skip the opcode...
                                // ...which lands us on the relative offset, a 32 bit value
                                // (yes, it is 32 bit even on a 64 bit build.)
                                auto relative = *(cast(int*) code);

                                // the actual address is the next instruction add + the value,
                                // so code+4 is address of next instruction, then + relative gets
                                // us the actual function address.
                                void* address = (code + 4) + relative;
                                auto func = cast(void function()) address;

                                // and run it, in a try/catch so we can handle failures.
                                try {
                                        func();
                                        import std.stdio;
                                        writeln("**Test Block Success**");
                                } catch(Throwable t) {
                                        import std.stdio;
                                        writeln("**Failure: ", t.file, ":", t.line, " ", t.msg);
                                        overallSuccess = false;
                                }

                                // move to the next instruction
                                code += 4;
                        }
                }
        }

        // returning false means main is never run. When doing a
        // unit test build, a lot of us feel running main is just
        // silly regardless of test passing, so I will always return
        // false.

        // You might want to do something like C exit(1) on failure instead
        // so a script can detect that.
        return false && overallSuccess;
}

如果需要,我会将调试符号拉出来打印文件+后续测试的行信息,作为读者练习。

我提供这个肮脏的黑客,希望它有用,但没有任何形式的保证,甚至不适合特定用途的适销性。

我在Linux上使用dmd进行了测试,它可能会或可能不会在其他地方运行,我不知道gdc和ldc是否生成相同的函数,或者它们的优化是否会产生影响等等。

如果你真的想要一个新的测试运行器,我建议使用支持的技术,比如构建工具或手动维护的模块列表以及编译时反射:单元线程库除此之外还做了很多漂亮的事情,所以看看吧。

但是,仅运行时选项也不是死路一条:)

答案 1 :(得分:0)

我说我发布了我提出的最简单的解决方案..好吧,不幸的是我无法用我的解决方案达到我想要的效果,所以毕竟我去了unit-threaded。我想做的方法是: 使用配音你可以对你的代码进行双重传递,如下所示:

    "configurations": [
    { "name": "executable" },
    {
        "name": "unittest",
        "targetType": "executable",
        "preBuildCommands": ["dub test -c gen_ut_main"],
        "sourceFiles": ["bin/generated_ut_main.d"],
    },
    {
        "name": "gen_ut_main",
        "targetType": "executable",
        "versions": ["GenUtMain"],
    }
]

这将导致配音测试&#39;它将首先执行目标gen_ut_main,其版本设置为GenUtMain。你可以这样做:

 version(GenUtMain) {
 void main() {
     writeln("gen ut main");
     generate_file();
}

generate_file然后可以生成新的&#39; .d&#39;文件,其中包含有关所有模块的信息。您可以使用&#39; ModuleInfo&#39;来实现它。因为ModuleInfo可以很好地在generate_file函数中使用运行时对象,或者作为单元线程执行此操作,您可以简单地遍历项目中的文件结构并获取所有.d文件,然后您就拥有了包列表。 ModuleInfo有1个警告,它会给你所有模块的列表,这可能不是你想要的,因为标准模块将嵌入单元测试,所以你不得不过滤掉这些,我不是能够过滤掉这些,我决定使用单元线程方法。

现在,在完成目标gen_ut_main之后,将重新生成bin / generated_ut_main.d,它将由unittest目标编译。通过这种方式,您可以按模块运行所有单元测试,自由检查UDA并能够在模块中运行所有单元测试,即使一个失败也是如此。

据说有一个包被测试过&#39;它与配音集成。经过测试可以让您列出所有模块,我无法使其正常工作。