32、Moles:借助闭包实现工具辅助的环境隔离

Moles:借助闭包实现工具辅助的环境隔离

1. 引入示例

让我们考虑一个在2000年1月1日抛出异常的C#方法:

public static class Y2KChecker {
    public static void Check() {
        if (DateTime.Now == new DateTime(2000,1,1))
            throw new ApplicationException("y2k bug!");
    }
}

测试这个方法存在问题,因为程序依赖于 DateTime.Now ,而它又依赖计算机时钟,具有环境依赖性和不确定性。而且, DateTime.Now 是静态属性,无法使用虚方法重写在测试时提供不同实现。这体现了单元测试中的隔离问题:直接调用数据库API、与Web服务通信的程序难以进行单元测试,因为它们的逻辑依赖于环境。

如果.NET允许在运行时重新定义静态方法的含义,我们可以通过用其他值替换 DateTime.Now 的计算值来解决测试问题,但以下代码无法编译:

// does not compile: DateTime.Now cannot be set
DateTime.Now = new DateTime(2000, 1, 1);

Moles可以解决这个问题。Mole类型提供了一种机制来绕过任何.NET方法。当一个方法被绕过,该方法的所有调用都会转发到一个替代实现,而原始方法不会被实际调用。Moles代码生成器创建了一个类型 MDateTime ,它有一个可设置的属性 NowGet ,可以将其设置为一个不接受参数并返回 DateTime 值的委托实例。

// attach delegate to the mole property to redirect DateTime.Now
// to return January 1st of 2000
MDateTime.NowGet = () => new DateTime(2000, 1, 1);
Y2KChecker.Check();

设置 MDateTime.NowGet 后,所有对 DateTime.Now 属性获取器的调用都会被重定向到用户提供的委托,这通过代码插桩实现。使用Visual Studio单元测试框架时,需要添加 [HostType("Moles")] 属性来启用代码插桩。

[TestMethod]
[HostType("Moles")] // run with code instrumentation
public void Y2kCheckerTest() {
    ...
}

Pex是一个用于.NET的自动化白盒测试生成工具,支持参数化单元测试。以下方法通过 [PexMethod] 属性被识别为参数化单元测试:

[PexMethod]
public void Y2kCheckerTest(DateTime time) {
    // hook to the method to redirect DateTime.Now
    MDateTime.NowGet = () => time;
    Y2KChecker.Check();
}

Pex通过符号跟踪测试输入,发现路径条件 time == new DateTime(2000,1,1) ,并生成两个传统单元测试:

[TestMethod]
[HostType("Moles")]
public void Y2kCheckerTest01() {
    Y2kCheckerTest(new DateTime(1, 1, 1));
}
[TestMethod]
[HostType("Moles")]
public void Y2kCheckerTest02() {
    Y2kCheckerTest(new DateTime(2000, 1, 1));
}

没有Moles,Pex无法构造覆盖代码所有情况的测试用例。

2. 背景知识:委托、Lambda表达式和闭包

Moles框架利用了.NET委托、C#匿名方法、Lambda表达式和闭包。

在.NET中,委托允许以完全类型安全的方式将函数指针表示为值并进行调用。委托值除了包含实际的函数指针,还可能包含一个对象引用,通常作为实例方法的接收者对象。委托类型指定了方法调用的返回类型和参数类型,并且有名称。例如:

delegate string D(int x, int y);

假设有一个类包含以下方法:

string Compute(int x, int y) {
    return (x+y).ToString();
}

创建委托实例的方式类似于C#中的构造函数调用:

D d = new D(this.Compute);

在C#中,调用委托的语法与调用常规方法类似, d(23,42) 将调用 this.Compute(23,42)

从C# 2.0开始支持匿名方法,允许内联 Compute 方法:

D d = delegate(int x, int y) {
    return (x+y).ToString();
};

从C# 3.0开始支持Lambda表达式,可进一步简化代码:

D d = (x, y) => (x+y).ToString();

匿名方法或Lambda表达式可以引用外部方法作用域内定义的局部变量。例如:

int offset = 42;
D d = (x, y) => (x + y + offset).ToString();

此时,C#编译器会生成一个闭包类来保存所有引用的局部变量,以下是编译器生成的等效代码:

Closure c = new Closure();
c.offset = 42;
D d = new D(c.Body);
class Closure {
    public int offset;
    public string Body(int x, int y) {
        return (x + y + offset).ToString();
    }
}

在C#中,匿名方法或Lambda表达式的主体可以修改捕获的局部变量,并且更改对同一变量的所有其他引用都是可见的,即C#中的闭包捕获变量,而不仅仅是值。

3. 我们的方法:Moles
3.1 代码插桩和底层绕过库

Moles框架基于扩展反射代码插桩框架来实现方法调用的绕过,扩展反射也被Pex和CHESS使用。代码插桩细节对用户隐藏,用户只需与生成的高级mole类型交互。

插桩过程如下:在每个方法的开头插入一些代码,该代码查询底层绕过库,看是否有绕过委托附加到该特定方法。如果有,则调用绕过委托;否则,执行正常代码。

例如,对于以下方法:

string Compute(int x, int y) {
    return (x + y).ToString();
}

插桩后的代码类似于以下伪代码:

string Compute(int x, int y) {
    // obtain identifier of this method
    Method thisMethod = this.GetType().GetMethod(
        "Compute", new Type[]{typeof(int), typeof(int)});
    // query if a detour has been attached
    Delegate detour = _Detours.GetDetour(this, thisMethod);
    if (detour != null) {
        // pseudo-code; actual implementation avoids boxing
        return (string)_Detours.InvokeDetour(
            detour, this,
            new object[] { x, y });
    }
    // else execute normal code
    return (x + y).ToString();
}

上述代码使用了一个底层绕过库 _Detours ,它管理所有附加的绕过委托,并提供查询和调用绕过委托的功能。

public class _Detours {
    // attaching and detaching
    public static void AttachDetour(
        object receiver, Method method,
        Delegate detourDelegate) {
        ...
    }
    public static void DetachDetour(
        object receiver, Method method) {
        ...
    }
    // quering and invoking
    public static Delegate GetDetour(
        object receiver, Method method) {
        ...
    }
    public static object InvokeDetour(
        Delegate detour,
        object receiver, object[] args) {
        ...
    }
}

插桩后的实际代码使用“不安全”的.NET指令避免了类型转换和装箱,提高了效率。

3.2 为类型安全生成Mole类型

通过通用绕过库附加和分离moles时,编译时无法保证类型安全。为了确保以类型安全的方式进行操作,Moles框架生成专门的代码。这不仅保证了类型安全,还使开发者无需使用.NET反射API构造方法标识符,并支持Visual Studio编辑器的自动代码完成。

对于每个类型 t ,会生成一个mole类型 Mt 。对于 t 中的每个方法,包括.NET属性的隐式getter和setter方法,以及.NET事件的添加和移除方法,都会生成可设置的mole属性。生成的类型 Mt 放在类型 t 的子命名空间中,命名空间后缀为 .Moles ,生成的类型放在单独的程序集中。

  • 静态方法和构造函数 :对于类型 t 中的每个静态方法,参数类型为 T1, T2, ..., Tn ,返回类型为 U ,在类型 Mt 中会生成一个可设置的静态属性,委托类型类似如下:
delegate U Func (T1, T2, ..., Tn)

实例构造函数的隐式 this 参数成为第一个参数 T1 。属性名以静态方法名开头,后面追加参数的短类型名,可能还会有一个数字以确保所有生成的属性名唯一。例如, MDateTime.NowGet 的生成实现如下:

static Func/*delegate DateTime Func()*/ NowGet {
    set { // property setter has implicit ’value’ parameter
        Method method = typeof(DateTime).GetMethod("get_Now");
        if (value == null)
            _Detours.DetachDetour(null, method);
        else
            _Detours.AttachDetour(null, method, value);
    }
}
  • 所有实例的实例方法 :对于类型 t 中的每个实例方法,显式参数类型为 T1, T2, ..., Tn ,返回类型为 U ,在嵌套类型 Mt.AllInstances 中会生成一个可设置的静态属性,委托类型类似如下:
delegate U Func (t, T1, T2, ..., Tn)

例如, Compute 方法的生成静态属性 ComputeInt32Int32 如下:

static Func/*delegate string Func(int,int)*/ ComputeInt32Int32 {
    set { // property setter has implicit ’value’ parameter
        Method method = typeof(ComputeType).GetMethod("Compute",
            new Type[] { typeof(int), typeof(int) });
        if (value == null)
            _Detours.DetachDetour(null, method);
        else
            _Detours.AttachDetour(null, method, value);
    }
}

这里 AttachDetour DetachAttach 的第一个参数为 null ,表示该绕过适用于所有实例。

  • 特定实例的实例方法 :可以根据 this 参数的值将绕过委托分配给不同的调用。 Mt 类型可实例化,每个 Mt 实例与一个 t 实例关联,并为 t 的所有实例方法提供可设置的实例属性。对于类型 t 中的每个实例方法,参数类型为 T1, T2, ..., Tn ,返回类型为 U ,在类型 Mt 中会生成一个可设置的实例属性,委托类型类似如下:
delegate U Func (T1, T2, ..., Tn)

例如, Compute 方法的生成属性 ComputeInt32Int32 如下:

Func/*delegate string Func(int,int)*/ ComputeInt32Int32 {
    set { // property setter has implicit ’value’ parameter
        Method method = typeof(ComputeType).GetMethod("Compute",
            new Type[] { typeof(int), typeof(int) });
        if (value == null)
            _Detours.DetachDetour(this.Instance, method);
        else
            _Detours.AttachDetour(this.Instance, method, value);
    }
}

这里 AttachDetour DetachDetour 的第一个参数为 this.Instance ,表示该绕过仅适用于与当前mole实例关联的特定实例。

以下是一个mermaid流程图,展示方法调用时的处理流程:

graph TD;
    A[方法调用] --> B{是否有绕过委托};
    B -- 是 --> C[调用绕过委托];
    B -- 否 --> D[执行正常代码];
3.3 形式化

Moles框架受Pex启发,Pex使用动态符号执行来跟踪程序执行的控制流和数据流,以收集路径条件。Moles增加了附加和分离绕过委托的能力,并修改了方法调用的含义。

我们考虑以下操作:
| 操作类型 | 操作指令 |
| ---- | ---- |
| 通用计算、赋值和条件控制流 | assign x := f(y1, y2, ..., yn)
if(x) goto PC |
| 附加和分离moles | mole M := (y, N)
mole (x, M) := (y, N)
unmole M
unmole (x, M) |
| 调用和返回 | x := call M(y1, y2, ..., yn)
ret y |

程序 P 有两个函数: startP : PROC →N 将方法映射到其第一条指令的偏移量, instr P : N →INSTR 将每个偏移量映射到关联的指令。程序从偏移量0开始。

程序语义由初始程序状态 initP : HEAP × STACK 和单条指令执行函数 stepP : HEAP × STACK →HEAP×STACK 描述。堆将值映射到值,栈是栈帧的序列,每个栈帧由当前程序计数器和局部变量到值的映射组成。

初始程序状态:

initP = ({→}, [(0, {→})])

通用计算、赋值和条件控制流的执行规则如下:

stepP (H, R ++ (PC , L))
where (instr P (PC ) = assign x := f(y1, y2, ..., yn))
= (H, R ++ (PC + 1, L ⊕{x →ˆf(L(y1, y2, ..., yn))}))
where ˆf is the built-in function computing f

stepP (H, R ++ (PC , L))
where (instr P (PC ) = if(x) goto PC ′)
=
  (H, R ++ (PC ′, L)) if L(x) is true
  (H, R ++ (PC + 1, L)) otherwise

如果当前指令是赋值操作,不改变堆,增加程序计数器,并将计算的值赋给局部变量;如果是条件控制流操作,根据条件值改变程序计数器。

Moles:借助闭包实现工具辅助的环境隔离

3.3 形式化(续)

下面详细解释这些操作的具体含义和执行过程。

  • 通用计算、赋值和条件控制流

    • 赋值操作 assign x := f(y1, y2, ..., yn) 指令将内置函数 f 计算得到的值赋给变量 x 。在执行 stepP 函数时,堆 H 保持不变,程序计数器 PC 加 1,并且将计算得到的值存储在局部变量映射 L 中。例如,如果 f 是一个简单的加法函数, assign x := f(y1, y2) 就相当于 x = y1 + y2
    • 条件分支操作 if(x) goto PC 指令根据变量 x 的值进行条件分支。如果 x 的值为真(在程序的语义中),则将程序计数器跳转到指定的 PC 位置;否则,程序计数器加 1 继续执行下一条指令。
  • 附加和分离 moles

    • 附加 mole 闭包 mole M := (y, N) 指令将一个 mole 闭包 (y, N) 附加到方法 M 上。这意味着当调用方法 M 时,控制将转移到闭包 (y, N) 所代表的函数。对于特定实例的方法, mole (x, M) := (y, N) 指令将闭包附加到实例 x 的方法 M 上。
    • 分离 mole unmole M 指令将附加到方法 M 上的 mole 闭包分离,使得方法 M 恢复正常调用。同样, unmole (x, M) 指令将特定实例 x 的方法 M 上的 mole 闭包分离。
  • 调用和返回

    • 方法调用 x := call M(y1, y2, ..., yn) 指令调用方法 M ,并将返回值存储在变量 x 中。在调用过程中,如果有 mole 闭包附加到方法 M 上,控制将转移到闭包所代表的函数;否则,执行方法 M 的正常代码。
    • 返回操作 ret y 指令从方法调用中返回,将值 y 作为返回值。

以下是一个简单的示例,展示这些操作的组合使用:

// 初始状态
initP = ({→}, [(0, {→})])

// 假设程序指令序列
instr P = {
    0: assign x := 1,
    1: assign y := 2,
    2: mole M := (y, N), // 附加 mole 闭包
    3: x := call M(y),
    4: unmole M, // 分离 mole 闭包
    5: ret x
}

// 执行过程
stepP (initP)
// 第一步:assign x := 1
// 堆不变,PC 变为 1,L = {x → 1}
// 状态变为 ({→}, [(1, {x → 1})])

stepP (({→}, [(1, {x → 1})]))
// 第二步:assign y := 2
// 堆不变,PC 变为 2,L = {x → 1, y → 2}
// 状态变为 ({→}, [(2, {x → 1, y → 2})])

stepP (({→}, [(2, {x → 1, y → 2})]))
// 第三步:mole M := (y, N)
// 堆更新,记录 M 与 (y, N) 的关联,PC 变为 3
// 状态变为 ({M → (y, N)}, [(3, {x → 1, y → 2})])

stepP (({M → (y, N)}, [(3, {x → 1, y → 2})]))
// 第四步:x := call M(y)
// 如果 M 有 mole 闭包,调用闭包 (y, N),并将结果存储在 x 中
// 假设闭包返回 3,PC 变为 4,L = {x → 3, y → 2}
// 状态变为 ({M → (y, N)}, [(4, {x → 3, y → 2})])

stepP (({M → (y, N)}, [(4, {x → 3, y → 2})]))
// 第五步:unmole M
// 堆更新,移除 M 与 (y, N) 的关联,PC 变为 5
// 状态变为 ({→}, [(5, {x → 3, y → 2})])

stepP (({→}, [(5, {x → 3, y → 2})]))
// 第六步:ret x
// 返回 x 的值 3

通过这种形式化的描述,我们可以精确地定义 Moles 框架的操作语义,使得开发者能够更好地理解和使用 Moles 来进行环境隔离和测试。

总结

Moles 框架为解决单元测试中的环境隔离问题提供了一种有效的方法。通过代码插桩和底层绕过库,Moles 能够将方法调用重定向到用户指定的委托,从而绕过依赖于环境的组件,使得测试用例可以在隔离的环境中执行。

  • 核心优势

    • 环境隔离 :Moles 允许开发者在测试时绕过依赖于系统环境的方法,如 DateTime.Now ,使得测试用例不依赖于实际的系统状态,提高了测试的可重复性和稳定性。
    • 类型安全 :通过生成专门的 mole 类型,Moles 确保了在附加和分离 detour 委托时的类型安全性,同时简化了开发者的操作,支持 Visual Studio 编辑器的自动代码完成。
    • 与 Pex 集成 :Moles 与 Pex 等自动化测试工具紧密集成,使得 Pex 能够生成覆盖代码所有情况的测试用例,提高了测试的覆盖率和质量。
  • 使用建议

    • 合理使用 detour 委托 :在使用 Moles 时,应该谨慎选择需要绕过的方法,并确保 detour 委托的实现符合测试的需求。避免过度使用 detour 委托,以免影响代码的可维护性。
    • 结合其他测试技术 :Moles 可以与其他测试技术,如单元测试框架、集成测试框架等结合使用,以实现更全面的测试覆盖。
    • 注意性能问题 :虽然 Moles 的代码插桩经过优化,但在某些情况下,过多的插桩可能会影响程序的性能。因此,在实际使用中应该注意性能问题,并进行必要的优化。

总的来说,Moles 是一个强大的工具,能够帮助开发者解决单元测试中的环境隔离问题,提高测试的效率和质量。通过合理使用 Moles,开发者可以更加轻松地编写高质量的测试用例,确保代码的可靠性和稳定性。

【EI复现】基于深度强化学习的微能源网能量管理与优化策略研究(Python代码实现)内容概要:本文围绕“基于深度强化学习的微能源网能量管理与优化策略”展开研究,重点利用深度Q网络(DQN)等深度强化学习算法对微能源网中的能量调度进行建模与优化,旨在应对可再生能源出力波动、负荷变化及运行成本等问题。文中结合Python代码实现,构建了包含光伏、储能、负荷等元素的微能源网模型,通过强化学习智能体动态决策能量分配策略,实现经济性、稳定性和能效的多重优化目标,并可能与其他优化算法进行对比分析以验证有效性。研究属于电力系统与人工智能交叉领域,具有较强的工程应用背景和学术参考价值。; 适合人群:具备一定Python编程基础和机器学习基础知识,从事电力系统、能源互联网、智能优化等相关方向的研究生、科研人员及工程技术人员。; 使用场景及目标:①学习如何将深度强化学习应用于微能源网的能量管理;②掌握DQN等算法在实际能源系统调度中的建模与实现方法;③为相关课题研究或项目开发提供代码参考和技术思路。; 阅读建议:建议读者结合提供的Python代码进行实践操作,理解环境建模、状态空间、动作空间及奖励函数的设计逻辑,同时可扩展学习其他强化学习算法在能源系统中的应用。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值