(Я знаю, это смехотворно длинный вопрос. Я пытался отделить вопрос от моего исследования до сих пор, поэтому его немного легче читать.)
Я запускаю свои модульные тесты с помощью MSTest.exe. Иногда я вижу эту ошибку теста:
В индивидуальном методе unit test: "Процесс агента был остановлен во время выполнения теста".
На весь тестовый прогон:
One of the background threads threw exception: System.NullReferenceException: Object reference not set to an instance of an object. at System.Runtime.InteropServices.Marshal.ReleaseComObject(Object o) at System.Management.Instrumentation.MetaDataInfo.Dispose() at System.Management.Instrumentation.MetaDataInfo.Finalize()
Итак, вот что мне нужно сделать: мне нужно отследить, что вызывает ошибку в MetaDataInfo, но я рисую пробел. Мой пакет unit test занимает более получаса, и ошибка не возникает каждый раз, поэтому трудно воспроизвести его.
Кто-нибудь еще видел этот тип сбоев при выполнении модульных тестов? Вы могли отслеживать его до определенного компонента?
Edit:
Проверяемый код представляет собой сочетание С#, С++/CLI и немного неуправляемого кода на С++. Неуправляемый С++ используется только из С++/CLI, никогда непосредственно из модульных тестов. Модульные тесты - все С#.
Тестирование кода будет выполняться в автономной службе Windows, поэтому никаких осложнений от ASP.net или чего-либо подобного нет. В тестируемом коде запускаются и останавливаются потоки, сетевая связь и ввод/вывод файлов на локальный жесткий диск.
Мое расследование до сих пор:
Я потратил некоторое время на копирование нескольких версий сборки System.Management на моей машине с Windows 7, и я нашел класс MetaDataInfo в System.Management в моем каталоге Windows. (Версия, которая в разделе Program Files\Reference Assemblies намного меньше, и не имеет класса MetaDataInfo.)
Используя Reflector для проверки этой сборки, я нашел то, что кажется очевидной ошибкой в MetaDataInfo.Dispose():
// From class System.Management.Instrumentation.MetaDataInfo:
public void Dispose()
{
if (this.importInterface == null) // <---- Should be "!="
{
Marshal.ReleaseComObject(this.importInterface);
}
this.importInterface = null;
GC.SuppressFinalize(this);
}
С помощью этого оператора "if" в обратном направлении MetaDataInfo пропустит COM-объект, если он есть, или выбросит исключение NullReferenceException, если нет. Я сообщил об этом в Microsoft Connect: https://connect.microsoft.com/VisualStudio/feedback/details/779328/
Используя отражатель, я смог найти все виды использования класса MetaDataInfo. (Это внутренний класс, поэтому просто поиск сборки должен быть полным списком.) Используется только одно место:
public static Guid GetMvid(Assembly assembly)
{
using (MetaDataInfo info = new MetaDataInfo(assembly))
{
return info.Mvid;
}
}
Поскольку все использования MetaDataInfo корректно устранены, вот что происходит:
- Если MetaDataInfo.importInterface не имеет значения null:
- статический метод GetMvid возвращает MetaDataInfo.Mvid
-
using
вызывает MetaDataInfo.Dispose- Утилизировать утечку COM-объекта
- Dispose устанавливает importInterface в null
- Утилизировать вызовы GC.SuppressFinalize
- Позже, когда GC собирает MetaDataInfo, финализатор пропускается.
- .
- Если MetaDataInfo.importInterface имеет значение null:
- статический метод GetMvid получает исключение NullReferenceException, вызывающее MetaDataInfo.Mvid.
- Прежде чем распространение распространяется,
using
вызывает MetaDataInfo.Dispose- Утилизировать вызовы Marshal.ReleaseComObject
- Marshal.ReleaseComObject создает исключение NullReferenceException.
- Поскольку выбрано исключение, Dispose не вызывает GC.SuppressFinalize
- Утилизировать вызовы Marshal.ReleaseComObject
- Исключение распространяется до получателя GetMvid.
- Позже, когда GC собирает MetaDataInfo, он запускает Finalizer
- Завершение вызовов Dispose
- Утилизировать вызовы Marshal.ReleaseComObject
- Marshal.ReleaseComObject генерирует исключение NullReferenceException, которое распространяется до GC, и приложение завершается.
- Утилизировать вызовы Marshal.ReleaseComObject
- Завершение вызовов Dispose
Для чего это стоит, вот остальная часть соответствующего кода из MetaDataInfo:
public MetaDataInfo(string assemblyName)
{
Guid riid = new Guid(((GuidAttribute) Attribute.GetCustomAttribute(typeof(IMetaDataImportInternalOnly), typeof(GuidAttribute), false)).Value);
// The above line retrieves this Guid: "7DAC8207-D3AE-4c75-9B67-92801A497D44"
IMetaDataDispenser o = (IMetaDataDispenser) new CorMetaDataDispenser();
this.importInterface = (IMetaDataImportInternalOnly) o.OpenScope(assemblyName, 0, ref riid);
Marshal.ReleaseComObject(o);
}
private void InitNameAndMvid()
{
if (this.name == null)
{
uint num;
StringBuilder szName = new StringBuilder {
Capacity = 0
};
this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
szName.Capacity = (int) num;
this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
this.name = szName.ToString();
}
}
public Guid Mvid
{
get
{
this.InitNameAndMvid();
return this.mvid;
}
}
Изменить 2:
Я смог воспроизвести ошибку в классе MetaDataInfo для Microsoft. Однако мое воспроизведение немного отличается от того, что я вижу здесь.
- Воспроизведение: Я пытаюсь создать объект MetaDataInfo в файле, который не является управляемой сборкой. Это генерирует исключение из конструктора до инициализации
importInterface
. - Моя проблема с MSTest: MetaDataInfo построена на некоторой управляемой сборке, и что-то происходит, чтобы сделать
importInterface
null или выйти из конструктора до того, какimportInterface
будет инициализирован.- Я знаю, что MetaDataInfo создается на управляемой сборке, потому что MetaDataInfo является внутренним классом, и единственный API, который его вызывает, делает это, передавая результат Assembly.Location.
Однако повторное создание проблемы в Visual Studio означало, что она загрузила источник в MetaDataInfo для меня. Вот реальный код, с оригинальными комментариями разработчиков.
public void Dispose()
{
// We implement IDisposable on this class because the IMetaDataImport
// can be an expensive object to keep in memory.
if(importInterface == null)
Marshal.ReleaseComObject(importInterface);
importInterface = null;
GC.SuppressFinalize(this);
}
~MetaDataInfo()
{
Dispose();
}
Исходный код подтверждает то, что было замечено в рефлекторе: оператор if обратный, и они не должны получать доступ к управляемому объекту из Finalizer.
Я уже говорил об этом, потому что он никогда не вызывал ReleaseComObject
, что он пропускал COM-объект. Я читал больше об использовании COM-объектов в .Net, и если я правильно его понимаю, это было неправильно: COM-объект не был выпущен при вызове Dispose(), но он освобождается, когда сборщик мусора приближается к сборщик Runtime Callable Wrapper, который является управляемым объектом. Несмотря на то, что это оболочка для неуправляемого COM-объекта, RCW по-прежнему является управляемым объектом, и правило "не обращаться к управляемым объектам из финализатора" должно применяться.