3

我正在使用 DUnit 和 FastMM 来捕获未完成的内存块,但似乎有一个错误。我不知道它是在 FastMM、DUnit 还是在 Delphi 本身中,但这里有:

  • 当我的测试用例有内部字符串时,测试会因内存泄漏而失败。如果我在不关闭 DUnit GUI 的情况下再次运行相同的测试,则测试通过。DUnit GUI 测试也会出现同样的情况,我相信出于同样的原因。我的应用程序中没有泄漏,证明 FastMM 在这些情况下不会生成泄漏报告。

  • 问题1:有没有办法在不设置 AllowedMemoryLeakSize 的情况下忽略它们

  • 问题2:我使用的是Delphi 7,如果这个修复在Delphi XE 中有什么消息吗?

  • 我的实际测试配置:

    • test.FailsOnNoChecksExecuted := True;
    • test.FailsOnMemoryLeak := True;
    • test.FailsOnMemoryRecovery := False;
    • test.IgnoreSetUpTearDownLeaks := True;

这是一个示例代码(仅实现)

    procedure TTest.Setup;
    begin
        A := 'test';
    end;

    procedure TTest.TearDown;
    begin
        // nothing here :)
    end;

    procedure TTest.Test;
    begin
        CheckTrue(True);
    end;

谢谢!!!!

更新:我面临的问题记录在http://members.optusnet.com.au/mcnabp/Projects/HIDUnit/HIDUnit.html#memoryleakdetection 但是除了运行相同的测试之外,相同的链接没有提供解决方案再次。

4

4 回答 4

1

我会首先尝试来自 Subversion 的当前版本(但此版本不适用于 Delphi 7,仅适用于 2007 和更新版本)

提交日志中,一个版本有一个关于该区域修复的评论。

修订版 40 已修改 2011 年 4 月 15 日星期五 23:21:27 UTC(14 个月前)

将 JclStartExcetionTracking 和 JclStopExceptionTracking 移出 DUnit 递归,以防止无效的内存泄漏报告

于 2012-06-18T17:21:05.590 回答
1

实际上,严格来说,您的测试在第一次运行时泄漏内存。 这不是FastMM、DUnit 或 Delphi 中的错误,错误在您的测试中。

让我们从澄清误解开始,并解释一些内部工作原理:

误解:FastMM 证明我的应用程序没有泄漏

这里的问题是,如果 FastMM 没有检测到泄漏,它会给你一种错误的安全感。原因是任何类型的泄漏检测都必须从检查点寻找泄漏。如果在开始检查点之后完成的所有分配都由结束检查点恢复 - 一切都很酷。

因此,如果您创建一个全局对象 Bin,并将所有对象发送到 Bin 而不破坏它们,那么您确实存在内存泄漏。继续运行,您的应用程序将耗尽内存。但是,如果 Bin 在 FastMM End 检查点之前销毁其所有对象,FastMM 将不会注意到任何不良情况。

您的测试中发生的情况是,FastMM 的检查点范围比 DUnit 泄漏检测范围更广。您的测试会泄漏内存,但该内存稍后会在 FastMM 进行检查时恢复。

每个 DUnit 测试都有自己的实例进行多次运行

DUnit 为每个测试用例创建一个单独的测试类实例。但是,每次运行测试都会重复使用这些实例。事件的简化顺序如下:

  • 启动检查点
  • 呼叫设置
  • 调用测试方法
  • 调用拆解
  • 结束检查点

因此,如果您在这 3 种方法之间存在泄漏 - 即使泄漏仅针对实例,并且会在对象被销毁后立即恢复 - 也会报告泄漏。在您的情况下,当对象被销毁时,泄漏被恢复。因此,如果 DUnit 为每次运行创建并销毁了测试类,则不会报告泄漏。

注意这是设计使然,因此您不能真正称其为错误。

基本上,DUnit 对您的测试必须 100% 自包含的原则非常严格。从 SetUp 到 TearDown,您分配(直接/间接)的任何内存都必须恢复。

常量字符串在分配给变量时被复制

每当您编码StringVar := 'SomeLiteralString'或复制常量StringVar := SomeConstStringStringVar := SomeResourceString值时(是的,已复制- 不计入引用)

同样,这是设计使然。目的是,如果从库中检索到字符串,在卸载库时不会丢弃该字符串。所以这不是一个真正的错误,只是一个“不方便”的设计。

因此,您的测试代码在第一次运行时泄漏内存的原因A := 'test'是为“测试”的副本分配内存。在随后的运行中,“测试”的另一个副本被制作,前一个副本被销毁 - 但净内存分配是相同的。

解决方案

在这种特殊情况下的解决方案是微不足道的。

procedure TTest.TearDown;
begin
  A := ''; //Remove the last reference to the copy of "test" and presto leak is gone :)
end;

一般来说,你不应该做更多的事情。如果您的测试创建了引用常量字符串副本的子对象,则这些副本将在子对象被销毁时被销毁。

但是,如果您的任何测试将对字符串的引用传递给任何全局对象/单例(顽皮,顽皮,您知道您不应该这样做),那么您将泄漏引用并因此泄漏一些内存 - 即使它是后来恢复了。

一些进一步的观察

回到关于 DUnit 如何运行测试的讨论。同一测试的单独运行可能会相互干扰。例如

procedure TTestLeaks.SetUp;
begin
  FSwitch := not FSwitch;
  if FSwitch then Fail('This test fails every second run.');
end;

扩展这个想法,您可以让您的测试在第一次和每秒(甚至)运行时“泄漏”内存。

procedure TTestLeaks.SetUp;
begin
  FSwitch := not FSwitch;
  case FSwitch of
    True : FString := 'Short';
    False : FString := 'This is a long string';
  end;
end;

procedure TTestLeaks.TearDown;
begin
  // nothing here :(  <-- note the **correct** form for the smiley
end;

这并不会真正导致内存的总体消耗增加,因为每次交替运行恢复的内存量与每次运行时泄漏的内存量相同。

字符串复制会导致一些有趣的(也许是意想不到的)行为。

var
  S1, S2: string;
begin
  S1 := 'Some very very long string literal';
  S2 := S1; { A pointer copy and increased ref count }
  if (S1 = S2) then { Very quick comparison because both vars point to the same address, therefore they're obviously equal. }
end;

然而....

const
  CLongStr = 'Some very very long string literal';
var
  S1, S2: string;
begin
  S1 := CLongStr;
  S2 := CLongStr; { A second **copy** of the same constant is allocated }
  if (S1 = S2) then { A full comparison has to be done because there is no shortcut to guarantee they're the same. }
end;

这确实提出了一个有趣但极端且可能不明智的解决方法,只是由于该方法的荒谬性:

const
  CLongStr = 'Some very very long string literal';
var
  GlobalLongStr: string;

initialization
  GlobalLongStr := CLongStr; { Creates a copy that is safely on the heap so it will be allowed to be reference counted }

//Elsewhere in a test
procedure TTest.SetUp;
begin
  FString1 := GlobalLongStr; { A pointer copy and increased ref count }
  FString2 := GlobalLongStr; { A pointer copy and increased ref count }
  if (FString1 = FString2) then { Very efficient compare }
end;

procedure TTest.TearDown;
begin
  {... and no memory leak even though we aren't clearing the strings. }
end;

最后/结论

是的,显然这篇冗长的帖子即将结束。

非常感谢您提出这个问题。
它给了我一个关于我记得不久前遇到的相关问题的线索。在我有机会证实我的理论之后,我会发布一个问答;因为其他人也可能会发现它很有用。

于 2013-09-08T21:06:51.677 回答
1

我找到了一种减轻问题的方法:我没有使用字符串,而是在测试类中使用了 ShortStrings 和 WideStrings。他们没有泄漏。

这不是解决方案,顺便说一句,这似乎在最新的 Delphi 版本中得到了解决。

于 2012-09-03T18:04:08.773 回答
0

底线是检测到的泄漏可能与正在执行的测试用例无关,但在检测到它时它是合法的泄漏。字符串的内存在进入 SetUp 过程之前未分配,并且在退出TearDown过程之前未释放。因此,在重新分配字符串变量或破坏测试用例之前,这是一个内存泄漏。

对于字符串和动态数组,您可以SetLength(<VarName>, 0)TearDown过程中使用。

于 2012-07-02T21:22:32.023 回答