Delphi 7, DUnit и FastMM сообщают о неверных строках
Я использую DUnit и FastMM для перехвата незавершенных блоков памяти, но, похоже, есть ошибка. Я не знаю, если это в FastMM, DUnit или в самой Delphi, но здесь идет:
Когда в моем тестовом примере есть внутренние строки, тест завершается с утечками памяти. Если я снова запускаю тот же тест, не закрывая графический интерфейс DUnit, тест проходит успешно. То же самое происходит с 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. Но та же ссылка не представляет решения, кроме выполнения того же теста снова.
4 ответа
На самом деле, строго говоря, ваш тест пропускает память при первом запуске.
Этоне ошибка в FastMM, DUnit или Delphi, ошибка в вашем тесте.
Давайте начнем с выяснения неправильных представлений и объяснения некоторых внутренних действий:
Заблуждение: FastMM доказывает, что в моем приложении нет утечек
Проблема здесь в том, что FastMM может дать вам ложное чувство безопасности, если он не обнаруживает утечки. Причина в том, что любой вид обнаружения утечек должен искать утечки с контрольных точек. При условии, что все распределения, сделанные после контрольной точки Start, восстановлены контрольной точкой End - все круто.
Таким образом, если вы создаете глобальный объект Bin и отправляете все объекты в Bin, не уничтожая их, у васдействительно есть утечка памяти. Продолжайте работать как и ваше приложение будет исчерпано памяти. Однако, если корзина уничтожит все свои объекты до контрольной точки FastMM End, FastMM не заметит ничего плохого.
Что происходит в вашем тесте, так это то, что FastMM имеет более широкий диапазон контрольных точек, чем DUnit. Ваш тест теряет память, но эта память позже восстанавливается к тому времени, когда FastMM выполняет свои проверки.
Каждый тест DUnit получает свой собственный экземпляр для нескольких прогонов
DUnit создает отдельный экземпляр вашего тестового класса для каждого тестового случая. Однако эти экземпляры используются повторно для каждого запуска теста. Упрощенная последовательность событий выглядит следующим образом:
- Начать контрольную точку
- Call SetUp
- Вызовите метод теста
- Call TearDown
- Конечная контрольная точка
Так что если у вас есть утечка между этими тремя методами - даже если утечка относится только к экземпляру и будет восстановлена, как только объект будет уничтожен - об утечке будет сообщено. В вашем случае утечка восстанавливается при разрушении объекта. Таким образом, если бы DUnit вместо этого создавал и уничтожал тестовый класс для каждого прогона, об утечке не сообщалось бы.
ПРИМЕЧАНИЕ. Это сделано специально, поэтому вы не можете назвать это ошибкой.
В основном DUnit очень строго придерживается принципа, что ваш тест должен быть на 100% самодостаточным. От SetUp до TearDown любая память, которую вы выделяете (прямо / косвенно), должна быть восстановлена.
Постоянные строки копируются всякий раз, когда они присваиваются переменной
Всякий раз, когда вы кодStringVar := 'SomeLiteralString'
или жеStringVar := SomeConstString
или же StringVar := 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;
Наконец / Заключение
Да, видимо, этот длинный пост закончится.
Большое спасибо за вопрос.
Это дало мне подсказку относительно связанной проблемы, которую я помню, переживая некоторое время назад. После того, как у меня будет возможность подтвердить свою теорию, я опубликую вопросы и ответы; как другие могут также найти это полезным.
Я нашел способ уменьшить проблему: вместо работы со строками я использовал ShortStrings и WideStrings в тестовых классах. Никаких утечек не появилось от них.
Это не решение, которое, кстати, кажется, решается в новейших версиях Delphi.
Я бы сначала попробовал текущую версию Subversion (но эта версия не работает с Delphi 7, только 2007 и новее):
В журнале коммитов одна версия имеет комментарий об исправлении в области
Редакция 40 Изменено Пт 15 апреля 23:21:27 UTC (14 месяцев назад)
переместить JclStartExcetionTracking и JclStopExceptionTracking из рекурсии DUnit, чтобы предотвратить появление недопустимых отчетов об утечке памяти
Суть в том, что обнаруженная утечка может не иметь отношения к выполняемому тестовому примеру, но это допустимая утечка в момент обнаружения. Память для строки была нераспределена до входа в процедуру SetUp, и она не была освобождена до выхода из TearDown
процедура. Таким образом, это утечка памяти до тех пор, пока строковая переменная не будет переназначена или контрольный пример не будет уничтожен.
Для строк и динамических массивов вы можете использовать SetLength(<VarName>, 0)
в TearDown
процедура.