2

Using a custom FXCop rule, I want to ensure that a method is called at the top of each unit test and that all unit test code is part of an Action passed into that method. Essentially I want this:

    [TestMethod]
    public void SomeTest()
    {
        Run(() => {
            // ALL unit test code has to be inside a Run call
        });
    }

It's not hard to ensure that Run is indeed called:

public override void VisitMethod(Method member)
    {
        var method = member as Method;
        if (method == null || method.Attributes == null)
            return;

        if (method.Attributes.Any(attr => attr.Type.Name.Name == "TestMethodAttribute") &&
            method.Instructions != null)
        {
             if (!method.Instructions.Any(i => i.OpCode == OpCode.Call || i.Value.ToString() == "MyNamespace.Run"))
            {
                this.Problems.Add(new Problem(this.GetResolution(), method.GetUnmangledNameWithoutTypeParameters()));
            }

        base.VisitMethod(method);
    }

The trick is to ensure there isn't something at the top of the test that is called BEFORE the Run statement. I've spent the past few hours hacking at the Instructions collection for patterns and trying to understand how to use the Body.Statements collection in code effectively.


This could also be posed as a simple IL question. I want to know a specific pattern I can validate that will accept this:

public void SomeTest()
{
    Run(() => {
        // Potentially lots of code
    });
}

But will reject either of these:

public void SomeTest()
{
    String blah = “no code allowed before Run”;
    Run(() => {
        // Potentially lots of code
    });
}

public void SomeTest()
{
    Run(() => {
        // Potentially lots of code
    });
    String blah = “no code allowed after Run”;
}
4

1 回答 1

1

尽管您可以使用类似结构的表达式树来访问,但Method.Body我可能还是会检查指令,因为我发现它会被过去的常见情况(例如内联数组初始化)弄糊涂。

C# 如何生成 lambda 表达式取决于 lambda 表达式访问的内容:

  • 如果 lambda 表达式访问任何局部变量/参数,它将创建一个对象来保存可以从 lambda 表达式访问它们的值,然后为对象上的实例方法创建一个委托。
  • 如果 lambda 表达式通过 访问任何实例成员this,那么它将简单地创建对实例方法的委托this
  • 否则,如果 lambda 表达式不访问本地变量或字段:
    • 在 VS2015 附带的 Roslyn 编译器之前,它会创建一个静态方法的委托并将其缓存在静态字段中。
    • 从 Roslyn 编译器开始,它会在嵌套类上创建实例方法,并将委托缓存在静态方法中。(此更改是出于性能原因,将委托调用到实例方法比将委托调用到静态方法更快,因为它不必重新调整参数。)

您可能可以创建自己的评估堆栈并通过该方法跟踪值(过去我不得不这样做,这不是微不足道的并且有相当多的代码但不是特别困难),但我怀疑您可以实现“足够好” " 只需执行以下规则:

  • 该方法的所有局部变量都必须是编译器生成的。
  • 只有编译器生成的静态字段才能被访问。
  • 只能System.Action创建编译器生成的类型。
  • 只能Run调用,而且必须只调用一次。
  • 调用后Run必须跟一个指令(忽略它们之间可能出现ret的任意数量的指令)。nop
  • 在调用Run.
  • 禁止除以下以外的所有指令:
    • 分支指令(有条件和无条件)
    • 参数、本地和字段访问器指令。
    • newobj, call, callvirt, ldftn, nop, ret,ldnull
    • _Locals(这只是 FxCop 为局部变量插入的伪指令。)

FxCop 提供RuleUtilities.IsCompilerGenerated了确定本地是否是编译器生成的,但它对字段没有帮助,如果 FxCop 可以找到 pdb 文件,我怀疑只有本地人。您可能会更容易说“本地/字段/类型是编译器生成的,如果它的类型名称不是 C# 中的有效标识符”。


说了这么多,坚持所有测试完全通过该Run方法运行似乎有点武断。如果目标是Run提供通用设置/拆卸逻辑的方法,则可以通过 nunit 提供更好的方法。强制人们应用您的操作属性比强制他们以特定方式编写测试要容易得多。

或者,您可以编写一个 Roslyn 分析器;分析器可以访问描述代码编写方式的语法树,而无需从 IL 和元数据中对结构进行逆向工程。

于 2014-11-23T03:40:01.420 回答