Delphi 7,DUnit和FastMM报告字符串错误

时间:2012-06-18 12:55:16

标签: delphi memory-leaks delphi-7 fastmm dunit

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

  • 当我的测试用例有内部字符串时,测试失败,内存泄漏。如果我在没有关闭DUnit GUI的情况下再次运行相同的测试,则测试通过OK。使用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中 但是,除了再次运行相同的测试之外,相同的链接不会提供解决方案。

4 个答案:

答案 0 :(得分:1)

我会首先从Subversion尝试当前版本的(但是这个版本不适用于Delphi 7,只有2007和更新版本)

commit log中,有一个版本对区域中的修复有评论

  

Revision 40 Modified Fri Apr 15 23:21:27 2011 UTC(14 months ago)

     

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

答案 1 :(得分:1)

我找到了一种减轻问题的方法:我没有使用Strings,而是在Test Classes中使用了ShortStrings和WideStrings。没有任何泄漏。

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

答案 2 :(得分:1)

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

让我们首先清除误解,并解释一些内部运作:

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

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

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

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

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

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

  • 启动检查点
  • 致电SetUp
  • 调用测试方法
  • 致电TearDown
  • 结束检查点

因此,如果这三种方法之间存在泄漏 - 即使泄漏仅发生在实例上,并且一旦对象被销毁就会恢复 - 将报告泄漏。在您的情况下,当对象被销毁时泄漏被恢复。因此,如果DUnit创造了&每次运行都会破坏测试类,不会报告任何泄漏。

  

注意这是设计使然,所以你不能把它称为错误。

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

只要将常量字符串赋值给变量

,就会复制它们

每当您对StringVar := 'SomeLiteralString'StringVar := SomeConstStringStringVar := 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如何运行测试的讨论。相同测试的单独运行可能相互干扰。 E.g。

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;

最后/结论

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

非常感谢您提出这个问题 它给了我一个关于我记得经历过一段时间的相关问题的线索。在我有机会证实我的理论之后,我会发布一个Q&amp;一个;其他人也可能觉得它很有用。

答案 3 :(得分:0)

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

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