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 以降のみ) :

コミット ログでは、1 つのバージョンにその領域の修正に関するコメントがあります。

リビジョン 40 変更済み Fri Apr 15 23:21:27 2011 UTC (14 か月前)

JclStartExcetionTracking と JclStopExceptionTracking を DUnit 再帰から移動して、無効なメモリ リーク レポートを防止します。

于 2012-06-18T17:21:05.590 に答える
1

問題を軽減する方法を見つけました。文字列を使用する代わりに、テスト クラスで ShortStrings と WideStrings を使用しました。それらから漏れはありませんでした。

ちなみに、最新の Delphi バージョンでは解決されているようです。

于 2012-09-03T18:04:08.773 に答える
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 := 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;

これは実際にはメモリの全体的な消費量を増加させるという結果にはなりません。これは、代替実行ごとに、2 回目の実行ごとにリークされたのと同じ量のメモリが回復されるためです。

文字列をコピーすると、興味深い (そしておそらく予期しない) 動作が発生します。

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 & A を投稿します。他の人も役に立つと思うかもしれません。

于 2013-09-08T21:06:51.677 に答える
0

要するに、検出されたリークは実行中のテスト ケースとは無関係である可能性がありますが、検出された時点では正当なリークであるということです。文字列のメモリは、SetUp プロシージャに入る前に割り当て解除され、TearDownプロシージャを終了する前に割り当て解除されません。したがって、文字列変数が再割り当てされるか、テスト ケースが破棄されるまで、メモリ リークが発生します。

文字列と動的配列の場合SetLength(<VarName>, 0)TearDownプロシージャで使用できます。

于 2012-07-02T21:22:32.023 に答える