Delphi 7、DUnit和FastMM错误地报告了字符串。

3
我正在使用DUnit和FastMM来捕获未释放的内存块,但似乎存在一个错误。我不知道这是FastMM、DUnit还是Delphi本身的问题,但请看以下情况:
当我的测试用例包含内部字符串时,测试在出现内存泄漏时失败。如果我不关闭DUnit GUI并再次运行相同的测试,则测试会通过。我认为DUnit GUI Testing也会出现相同的问题。我的应用程序中没有泄漏,证明在这些情况下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中有文档记录。 但是同样的链接没有提供其他解决方案,除了再次运行相同的测试。

抱歉,伙计,没有代码。像上面那样的简单测试在第一次运行时会产生泄漏。在第二次运行中,泄漏会消失。 - Rafael Castro
David,如果你打开一个新的单元测试,请在测试类中添加一个私有字符串,并将上面的代码粘贴进去,这样会不会对你造成泄漏? - Rafael Castro
事实上,DUnit使用FastMM,但还有其他我不确定的东西。正常的FastMM报告显示我的代码中没有泄漏。 - Rafael Castro
这一定是旧版本的DUnit,更新版本只能在Delphi 2007及更高版本中使用。这是哪个版本号的DUnit? - mjn
我的公司使用从http://dunit.sourceforge.net/#Download下载的9.3.0版本。 - Rafael Castro
显示剩余4条评论
4个回答

2
实际上,严格来说,您的测试在第一次运行时会泄漏内存。这不是FastMM、DUnit或Delphi中的错误,而是在您的测试中出现了错误。
让我们先澄清一些误解并解释一些内部工作原理:
误解:FastMM证明了我的应用程序中没有泄漏
问题在于,如果FastMM没有检测到泄漏,它可能会给您一种虚假的安全感。原因是任何类型的泄漏检测都必须从检查点寻找泄漏。只要在Start检查点之后进行的所有分配都在End检查点之前恢复 - 一切都很好。
所以,如果您创建了一个全局对象Bin,并将所有对象发送到Bin而不销毁它们,则会存在内存泄漏。继续运行,您的应用程序将耗尽内存。但是,如果Bin在FastMM End检查点之前销毁了其所有对象,则FastMM不会注意到任何不寻常的情况。
在您的测试中发生的情况是,FastMM的检查点范围比DUnit泄漏检测更广。您的测试会泄漏内存,但该内存稍后会在FastMM进行检查时恢复。
每个DUnit测试都为多次运行创建自己的实例
DUnit为每个测试用例创建测试类的单独实例。但是,这些实例会在每次运行测试时重复使用。事件的简化序列如下:
- Start检查点 - 调用SetUp - 调用测试方法 - 调用TearDown - End检查点
因此,如果您在这3个方法之间有泄漏 - 即使泄漏仅针对实例,并且一旦对象被销毁就会恢复 - 也会报告泄漏。在您的情况下,当对象被销毁时,泄漏会得到恢复。因此,如果DUnit为每次运行创建和销毁测试类,则不会报告任何泄漏。
常量字符串在分配给变量时会被复制
无论何时编写StringVar:='SomeLiteralString' 或 StringVar:=SomeConstString 或 StringVar:=SomeResourceString,常量的值都会被复制(是的,复制 - 而不是引用计数)
再次说明,这是根据设计而来。意图是,如果该字符串是从库中检索的,则不希望在卸载库时破坏该字符串。因此,这不是一个错误,只是一个“不方便”的设计。

这段测试代码第一次运行时会泄漏内存的原因是A := 'test'为"test"分配了内存。在后续运行中,又会创建另一个"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;

最后 / 总结

是的,显然这篇长篇大论的帖子要结束了。

非常感谢您提出这个问题。
它给了我一个线索,让我想起了一段时间以前遇到的一个相关问题。在我确认我的理论之后,我将发布一个问答,因为其他人可能也会发现它有用。


1
我找到了一个减轻问题的方法:在测试类中使用ShortStrings和WideStrings代替Strings。它们没有出现任何泄漏。
这不是解决方案,顺便说一下,在最新的Delphi版本中似乎已经解决了这个问题。

1

我会先尝试Subversion的当前版本(但此版本不支持Delphi 7,只支持2007及更新版本)

提交日志中,有一个版本涉及到该领域的修复注释。

版本号40,修改于2011年4月15日23:21:27 UTC(14个月前)

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


不幸的是,这不是原因。我删除了所有 JCL 引用(因为我根本不使用它),但内存泄漏仍然存在。 - Rafael Castro

0

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接