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

D execute all tests in module even if one fails

我正在尝试编写自己的 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 returns void 函数,它是一个将执行模块中所有单元测试的函数。有没有办法列出这些单元测试并迭代每个单元测试?这不起作用:

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

这样我就可以抓取所有单元测试,然后自由地迭代它们。 说 'm' 是一个变量而不是一个模块。我找不到任何文档什么是 'ModuleInfo' 实际上我只是错误地找到了这个...

好吧,反正我在评论里写了一半的答案,我想我也不妨在这里多写一点。

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

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

但是,我想在 运行 时间完成,就像您在问题中所做的那样,只是更详细一些。如何?通过做一些低级指针的东西!做一个老汇编语言黑客有时还是值得的:)

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

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;
}

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

我提供这个肮脏的 hack 是希望它有用,但没有任何形式的保证,甚至没有针对特定用途的适销性或适用性。

我在 Linux 上用 dmd 测试过,它可能在其他地方工作也可能不工作,我不知道 gdc 和 ldc 是否生成相同的函数,或者它们的优化是否有所不同等。

如果你真的想要一个新的测试,我建议使用支持的技术,比如构建工具或手动维护的模块列表以及编译时反射 运行纳尔:单元线程库做了很多漂亮的事情还有其他的东西,所以一定要检查一下。

但是 运行time-only 选项也不是死胡同 :)

我说过我会post我想出的最简单的解决方案..好吧,不幸的是我无法用我的解决方案实现我想要的,所以毕竟我去了并使用了unit-threaded.我想做的方法是: 使用 dub 你可以对你的代码进行双重传递,像这样:

    "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"],
    }
]

这将导致 'dub test' 它将首先执行目标 gen_ut_main,并将 Version 设置为 GenUtMain。有了它你可以做这样的事情:

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

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

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

据说有一个与 dub 集成的包 'tested'。经过测试,您应该可以列出所有模块,但我无法让它工作。