首页 文章

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

提问于
浏览
0

我正在尝试编写自己的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);
 }

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

2 回答

  • 1

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

    有一个很好的工作,让您可以访问所有模块 . 您可以尝试遍历导入图,但此方法无法使用本地导入 . 您可以使用构建工具列出您的模块,但这当然要求您实际使用具有该功能的构建工具(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是否生成相同的函数,或者它们的优化是否会产生影响等等 .

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

    但是,仅运行时选项也不是一个死胡同:)

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

    这将导致'dub test'它将首先执行目标gen_ut_main并将Version设置为GenUtMain . 你可以这样做:

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

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

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

    据说有一个'测试'包与dub集成 . 经过测试可以让您列出所有模块,我无法使其正常工作 .

相关问题