Разница в выполнении кода, когда метод расширения присутствует, но не вызывается

TL; DR, вопрос:

Какое влияние на выполнение кода может иметь наличие метода расширения в .NET(например, JIT/optimizations)?

Фон

Я испытываю неудачу теста в MSTest, которая зависит от того, проверена ли, по-видимому, неродственная сборка.

Я заметил ошибку теста и случайно заметил, что сбой произошел только в том случае, если была загружена другая тестовая сборка. Запуск mstest на сборках тестов Unittests и Integration начнет выполнение тестов интеграции и завершится неудачей в 21-м тесте интеграции в 4.5 CLR, тогда как это не произойдет в версии 4.0 CLR (в той же конфигурации в противном случае). Я удалил все тесты, но отказался от сборки тестирования интеграции. Выполнение теперь выглядит так, как при загрузке обеих тестовых сборок, mstest загружает обе сборки, а затем выполняет один тест в сборке тестов интеграции, что не удается.

> mstest.exe /testcontainer:Unittests.dll /testcontainer:IntegrationTests.dll

Loading C:\Proj\Tests\bin\x86\Release\Unittests.dll...
Loading C:\Proj\Tests\bin\x86\Release\Integrationtests.dll...
Starting execution...

Results               Top Level Tests
-------               ---------------
Failed                Proj.IntegrationTest.IntegrationTest21

Без сборки Unittests при выполнении теста проходит.

> mstest.exe /testcontainer:IntegrationTests.dll

Loading C:\Proj\Tests\bin\x86\Release\Integrationtests.dll...
Starting execution...

Results               Top Level Tests
-------               ---------------
Passed                Proj.IntegrationTest.IntegrationTest21

Я думал, что это должна быть вещь [AssemblyInitialize], выполняемая в dll UnitTests, или, возможно, какое-то статическое состояние в Unittest.dll или общая зависимость, которая изменяется при загрузке тестовой сборки. В Unittests.dll я не нахожу ни статических конструкторов, ни сборки init. Я подозревал разницу в развертывании, когда сборка Unittests была включена, (зависимая сборка была развернута в другой версии и т.д.), Но я сравнивал управляющие/отказоустойчивые серверы развертывания, и они являются двоичными эквивалентами.

Итак, какая часть сборки Unittests вызывает разницу в тестах? Из модульных тестов я удалял половину тестов за раз, пока я не свернул его до исходного файла в сборке модулей. Наряду с тестовым классом объявляется метод расширения:

Помимо этого класса расширений сборка Unittest теперь содержит один тестовый пример в фиктивном тестовом классе. Сбой теста происходит только в том случае, если у меня есть метод фиктивного теста и объявлен метод расширения. Я мог бы удалить всю оставшуюся тестовую логику, пока DLL Unittest не будет одним файлом, содержащим это:

// DummyTest.cs in Unittests.dll
[TestClass]
public class DummyTest
{
    [TestMethod]
    public void TestNothing()
    {
    }
}

public static class IEnumerableExtension
{
   public static IEnumerable<T> SymmetricDifference<T>(
       this IEnumerable<T> @this,         
       IEnumerable<T> that) 
   {
      return @this.Except(that).Concat(that.Except(@this));
   }
}

Если либо тестовый метод, либо класс расширения удалены, тест проходит. Оба присутствуют, и тест терпит неудачу.

Нет вызовов метода расширения, сделанного из любой сборки, и ни один код не выполняется в сборке Unittests до того, как будут выполнены тесты интеграции (насколько мне известно).

Я уверен, что интеграционный тест достаточно сложный, что различия в оптимизации JIT могут привести к различию, например. в плавающей запятой. Это то, что я вижу?

Ответ 1

Вероятно, проблема возникает из-за ошибки загрузки типа.

Когда среда CLR загружает класс или метод, он всегда проверяет все типы, используемые в этих элементах. Неважно, действительно ли тип/метод вызывается или нет. Важен факт объявления. Возвращаясь к вашему образцу, метод расширения SymmetricDifference объявляет, что использует методы Except и Concat из сборки System.Core.

Что происходит с ошибкой при загрузке типа System.Linq.Enumerable из сборки System.Core.

Причины такого поведения могут различаться. Самый первый шаг, который нужно предпринять, - это зарегистрировать точное исключение, которое вы получите при ошибке тестирования.

Ответ 3

Две мысли...

  • Математика и сравнение с плавающей запятой могут фактически давать разные результаты с оптимизацией/без нее. По крайней мере, они могут в С++ Они также могут и определенно будут отличаться на разных архитектурах процессора. Вообще говоря, это звучит как плохая идея иметь оценку w/w unit test, основанную на сравнении с плавающей запятой. Например, ваш друг, работающий с 64-разрядными окнами, не выполняет тесты модулей, пока вы преуспеваете.

  • Эта проблема с методом расширения - это охота на диких гусей. Так что, если это загадочно работает, если вы его удалите? Оптимизированный код иногда сумасшедший дерьмо, в конечном итоге проблема заключается в том, что ваш номер с плавающей запятой является РАЗЛИЧНЫМ с момента его неоптимизации. Добавьте epsilon, если разница неизбежна, иначе зарегистрируйте его значение на каждом шаге по пути его вычисления и отследите его.