Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)

Visual Studio - Fine Code Coverage에서 동작하지 않는 Fake/Shim 테스트

이런 코드 커버리지 도구가 있군요.

Fine Code Coverage
; https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage

음... 별로 매력적인 부분을 찾을 수 없는데, 왜 Visual Studio의 기본 Code Coverage를 놔두고 이걸 사용하는 걸까요? 혹시 장점을 아시는 분은 덧글 부탁드립니다. ^^

일단, 설치는 Visual Studio 2022에서도 할 수 있습니다. 그런데, 이게 한 가지 문제가 있는데요, 바로 Fake/Shim을 지원하지 못한다는 점입니다.

실제로 해당 코드가 들어간 단위 테스트를 작성하면, FCC 창에서 다음과 같은 오류를 확인할 수 있습니다.

  Failed TestTest [19 ms]
  Error Message:
   Test method ClassLibrary1.Tests.Class1Tests.TestTest threw exception: 
Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.UnitTestIsolationException: Failed to resolve profiler path from COR_PROFILER_PATH and COR_PROFILER environment variables.
  Stack Trace:
      at Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.IntelliTraceInstrumentationProvider.ResolveProfilerPath()
   at Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.IntelliTraceInstrumentationProvider.Initialize()
   at Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.UnitTestIsolationRuntime.InitializeUnitTestIsolationInstrumentationProvider()
   at Microsoft.QualityTools.Testing.Fakes.Shims.ShimRuntime.CreateContext()
   at Microsoft.QualityTools.Testing.Fakes.ShimsContext.Create()
   at ClassLibrary1.Tests.Class1Tests.TestTest() in C:\Users\SeongTae Jeong\Dropbox\articles\fin_code_coverage\net6_fakes_sample\ClassLibrary1Tests\Class1Tests.cs:line 19
Failed!  - Failed:     1, Passed:     1, Skipped:     0, Total:     2, Duration: 42 ms - ClassLibrary1Tests.dll (net6.0)

왜냐하면 Fake/Shim은 .NET Profiler의 IL Rewriter 기능을 이용해 작성되기 때문인데, 저렇게 Profiler 로딩을 못하므로 실패한 것입니다. 아마도 "{324F817A-7420-4E6D-B3C1-143FBED6D855}" GUID에 해당하는 Profiler로 여겨지는데, Visual Studio에서 제공하는 Code Coverage에서는 저 프로파일러가 잘 로딩이 되지만 Fine Code Coverage에서는 로딩에 실패하고 있는 것입니다. (이유는 잘 모르겠습니다.)

암튼, 저렇게 되면 ShimsContext.Create 호출부터 오류가 발생할 것이기 때문에 Code Coverage가 제대로 될 수 없습니다.




약간의 원인 분석을 해보자면.

Visual Studio가 실행하는 vstest.console.exe는 하위 프로세스로 testhost.exe를 통해 단위 테스트를 수행합니다. 이때 해당 프로세스에는 PROFILER 관련 환경 변수 설정들이 동적으로 추가되는데요,

SET CORECLR_PROFILER={324F817A-7420-4E6D-B3C1-143FBED6D855}
SET CORECLR_PROFILER_PATH_64=C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Platform\InstrumentationEngine\x64\MicrosoftInstrumentationEngine_x64.dll
SET CORECLR_ENABLE_PROFILING=1

SET COR_PROFILER={324F817A-7420-4E6D-B3C1-143FBED6D855}
SET COR_PROFILER_PATH_64=C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Platform\InstrumentationEngine\x64\MicrosoftInstrumentationEngine_x64.dll
SET COR_ENABLE_PROFILING=1

SET MicrosoftInstrumentationEngine_ConfigPath64_FakesInstrumentation=C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\TestPlatform\Extensions\Fakes\x64\FakesInstrumentationProfiler_x64.config

/* FakesInstrumentationProfiler_x64.config
<?xml version="1.0" encoding="utf-8"?>
<InstrumentationEngineConfiguration>
  <InstrumentationMethod>
    <Name>Fakes Instrumentation Method</Name>
    <Description>Instrumentation method to support Microsoft Fakes</Description>
    <Module>Microsoft.QualityTools.Testing.Fakes.Instrumentation.dll</Module>
    <ClassGuid>{F02C3E96-F6FD-4552-9544-9F06BE6E5A0B}</ClassGuid>
    <Priority>11</Priority>
  </InstrumentationMethod>
</InstrumentationEngineConfiguration>
*/

반면 FineCodeCoverage는 coverlet.exe를 통해 몇 층의 dotnet.exe를 거쳐 testhost.exe를 실행하게 되는데 이 계층 구조에서는 당연히 PROFILER 관련 환경 변수 설정이 없습니다. 그래서, 혹시나 싶어 명령행을 실행해 위의 COR_..., CORECLR_... 환경 변수 설정을 한 후 이를 상속받을 수 있도록 devenv.exe를 실행시켜 Code Coverage를 수행해 봤습니다.

그래서 분명히 환경 변수가 적용까지는 되지만 아쉽게도, 여전히 이런 오류가 발생합니다.

  Failed TestTest [26 s]
  Error Message:
   Test method ClassLibrary1.Tests.Class1Tests.TestTest threw exception: 
Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.UnitTestIsolationException: Unexpected error returned by SetDetourProvider in profiler library 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\TestPlatform\Extensions\Fakes\x64\Microsoft.QualityTools.Testing.Fakes.Instrumentation.dll'.
  Stack Trace:
      at Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.IntelliTraceInstrumentationProvider.Initialize()
   at Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.UnitTestIsolationRuntime.InitializeUnitTestIsolationInstrumentationProvider()
   at Microsoft.QualityTools.Testing.Fakes.Shims.ShimRuntime.CreateContext()
   at Microsoft.QualityTools.Testing.Fakes.ShimsContext.Create()
   at ClassLibrary1.Tests.Class1Tests.TestTest() in C:\temp2\ClassLibrary1Tests\Class1Tests.cs:line 27
Failed!  - Failed:     1, Passed:     1, Skipped:     0, Total:     2, Duration: 26 s - ClassLibrary1Tests.dll (net6.0)

이때 뜬 FineCodeCoverage의 testhost.exe에는 MicrosoftInstrumentationEngine_x64.dll과, "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\TestPlatform\Extensions\Fakes\x64\Microsoft.QualityTools.Testing.Fakes.Instrumentation.dll" 파일이 로딩된 것을 확인할 수 있습니다.

그런데 웬일인지 SetDetourProvider를 실행하지 못하고 있는 것입니다. 위의 코드가 실행되는 ".\bin\Debug\net6.0\Microsoft.QualityTools.Testing.Fakes.dll" 파일을 Reflector로 열어 보면 다음의 코드에서 오류가 발생하고 있는데요,

// Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.IntelliTraceInstrumentationProvider
// Token: 0x060000FE RID: 254 RVA: 0x0000382C File Offset: 0x00001A2C
public void Initialize()
{
    string text = this.ResolveProfilerPath();
    this.profilerModule = IntelliTraceInstrumentationProvider.LoadProfilerModule(text);
    this.setDetourProvider = LibraryMethods.GetFunction<NativeSetDetourProvider>(this.profilerModule, "SetDetourProvider");
    this.canDetour = LibraryMethods.GetFunction<NativeCanDetour>(this.profilerModule, "CanDetour");
    if (this.setDetourProvider(IntelliTraceInstrumentationProvider.detourProviderAddress) != 0)
    {
        throw new UnitTestIsolationException(string.Format(CultureInfo.CurrentCulture, FakesFrameworkResources.FailedToSetDetourProvider, text));
    }
    this.enabled = true;
}

public static T GetFunction<T>(IntPtr hModule, string functionName) where T : class
{
    return (T)((object)Marshal.GetDelegateForFunctionPointer(LibraryMethods.GetProcAddress(hModule, functionName), typeof(T)));
}

ResolveProfilerPath의 코드를 통해,

private string ResolveProfilerPath()
{
    string environmentVariable = Environment.GetEnvironmentVariable((IntPtr.Size == 8) ? "MicrosoftInstrumentationEngine_ConfigPath64_FakesInstrumentation" : "MicrosoftInstrumentationEngine_ConfigPath32_FakesInstrumentation", EnvironmentVariableTarget.Process);
    if (File.Exists(environmentVariable))
    {
        XmlReaderSettings settings = new XmlReaderSettings
        {
            DtdProcessing = DtdProcessing.Prohibit,
            XmlResolver = null
        };
        XmlReader xmlReader = XmlReader.Create(environmentVariable, settings);
        if (xmlReader.ReadToDescendant("Module"))
        {
            string path = xmlReader.ReadInnerXml();
            return Path.Combine(Path.GetDirectoryName(environmentVariable), path);
        }
    }
    ...[생략]...
    throw new UnitTestIsolationException(FakesFrameworkResources.FailedToResolveProfilerPath);
}

FakesInstrumentationProfiler_x64.config에 있는 Module 노드의 값, 즉 "Microsoft.QualityTools.Testing.Fakes.Instrumentation"을 가져오게 되고, 결국 다음의 DLL에서,

C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\TestPlatform\Extensions\Fakes\x64\Microsoft.QualityTools.Testing.Fakes.Instrumentation.dll      

SetDetourProvider를 구하게 됩니다. 물론, 이 파일에는,

C:\temp> dumpbin /EXPORTS "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\TestPlatform\Extensions\Fakes\x64\Microsoft.QualityTools.Testing.Fakes.Instrumentation.dll"
...[생략]...
    ordinal hint RVA      name

          1    0 000088A0 CanDetour
          2    1 00001920 DllCanUnloadNow
          3    2 00001930 DllGetClassObject
          4    3 00001920 DllRegisterServer
          5    4 00001920 DllUnregisterServer
          6    5 00008800 SetDetourProvider
...[생략]...

해당 API 함수를 정상적으로 export 하고 있습니다. 따라서 거기까지는 완료를 했는데, 아쉽게도 this.setDetourProvider 호출에서 오류가 발생한 것입니다.

// Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.IntelliTraceInstrumentationProvider
// Token: 0x060000FE RID: 254 RVA: 0x0000382C File Offset: 0x00001A2C
public void Initialize()
{
    string text = this.ResolveProfilerPath();
    this.profilerModule = IntelliTraceInstrumentationProvider.LoadProfilerModule(text);
    this.setDetourProvider = LibraryMethods.GetFunction<NativeSetDetourProvider>(this.profilerModule, "SetDetourProvider");
    this.canDetour = LibraryMethods.GetFunction<NativeCanDetour>(this.profilerModule, "CanDetour");
    if (this.setDetourProvider(IntelliTraceInstrumentationProvider.detourProviderAddress) != 0)
    {
        throw new UnitTestIsolationException(string.Format(CultureInfo.CurrentCulture, FakesFrameworkResources.FailedToSetDetourProvider, text));
    }
    this.enabled = true;
}

여기서 IntelliTraceInstrumentationProvider.detourProviderAddress가 가리키는 주소는,

private static readonly IntPtr detourProviderAddress = typeof(IntelliTraceInstrumentationProvider).GetMethod("DetourProvider", BindingFlags.Static | BindingFlags.NonPublic).MethodHandle.GetFunctionPointer();

internal static void DetourProvider(object receiver, RuntimeMethodHandle methodHandle, RuntimeTypeHandle declaringTypeHandle, RuntimeTypeHandle[] genericMethodTypeArgumentHandles, out object detourDelegate, out IntPtr detourPointer)
{
    detourDelegate = null;
    detourPointer = IntPtr.Zero;
    if (IntelliTraceInstrumentationProvider.ProtectingContext.IsThreadProtected)
    {
        return;
    }
    using (new IntelliTraceInstrumentationProvider.ProtectingContext())
    {
        MethodBase methodBase = MethodBase.GetMethodFromHandle(methodHandle, declaringTypeHandle);
        if (methodBase.IsGenericMethodDefinition)
        {
            Type[] array = new Type[genericMethodTypeArgumentHandles.Length];
            for (int i = 0; i < genericMethodTypeArgumentHandles.Length; i++)
            {
                array[i] = Type.GetTypeFromHandle(genericMethodTypeArgumentHandles[i]);
            }
            methodBase = ((MethodInfo)methodBase).MakeGenericMethod(array);
        }
        detourDelegate = UnitTestIsolationRuntime.GetDetour(receiver, methodBase);
        if (detourDelegate != null)
        {
            detourPointer = detourDelegate.GetType().GetMethod("Invoke").MethodHandle.GetFunctionPointer();
        }
    }
}

위와 같은데, 따라서 SetDetourProvider에 저 메서드(DetourProvider)를 전달한 다음 내부에서 뭔가 동작이 있었는데 거기서 알 수 없는 오류가 발생하고 있는 것입니다. 일단, 더 이상 추적할 수 없으니, 안 되는 걸로 ^^ 종료합니다. 뭔가 잡힐 듯하다가 놓치고 마니 아쉽군요.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/20/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 91  92  93  94  [95]  96  97  98  99  100  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11593정성태7/8/201820171Math: 40. GeoGebra 기하 (17) - 각의 복사파일 다운로드1
11591정성태7/7/201819160Math: 39. GeoGebra 기하 (16) - 삼각형의 방심과 방접원파일 다운로드1
11590정성태7/7/201818931Math: 38. GeoGebra 기하 (15) - 삼각형의 수심파일 다운로드1
11589정성태7/7/201818559.NET Framework: 787. object로 형변환된 인스턴스를 원래의 타입 인자로 제네릭 메서드를 호출하는 방법 [2]파일 다운로드1
11588정성태7/7/201820104디버깅 기술: 116. windbg 분석 사례 - ASP.NET 웹 응용 프로그램의 CPU 100% 현상 (3)
11587정성태7/5/201820520.NET Framework: 786. ASP.NET - HttpCookieCollection을 다중 스레드에서 사용할 경우 무한 루프 현상
11586정성태7/5/201818952Math: 37. GeoGebra 기하 (14) - 삼각형의 무게 중심파일 다운로드1
11585정성태7/5/201819652Math: 36. GeoGebra 기하 (13) - 삼각형의 외심과 외접하는 원파일 다운로드1
11584정성태7/5/201818933Math: 35. GeoGebra 기하 (12) - 삼각형의 내심과 내접하는 원파일 다운로드1
11583정성태7/5/201819570.NET Framework: 785. public으로 노출되지 않은 다른 어셈블리의 delegate 인스턴스를 Reflection으로 생성하는 방법파일 다운로드1
11582정성태7/5/201825551.NET Framework: 784. C# - 제네릭 인자를 가진 타입을 생성하는 방법 [1]파일 다운로드1
11581정성태7/4/201822573Math: 34. GeoGebra 기하 (11) - 3대 작도 불능 문제의 하나인 임의 각의 3등분파일 다운로드1
11580정성태7/4/201819593Math: 33. GeoGebra 기하 (10) - 직각의 3등분파일 다운로드1
11579정성태7/4/201818222Math: 32. GeoGebra 기하 (9) - 임의의 선분을 한 변으로 갖는 정삼각형파일 다운로드1
11578정성태7/3/201818320Math: 31. GeoGebra 기하 (8) - 호(Arc)의 이등분파일 다운로드1
11577정성태7/3/201818062Math: 30. GeoGebra 기하 (7) - 각의 이등분파일 다운로드1
11576정성태7/3/201821099Math: 29. GeoGebra 기하 (6) - 대수의 4칙 연산파일 다운로드1
11575정성태7/2/201821562Math: 28. GeoGebra 기하 (5) - 선분을 n 등분하는 방법파일 다운로드1
11574정성태7/2/201820072Math: 27. GeoGebra 기하 (4) - 선분을 n 배 늘이는 방법파일 다운로드1
11573정성태7/2/201818926Math: 26. GeoGebra 기하 (3) - 평행선
11572정성태7/1/201817942.NET Framework: 783. C# 컴파일러가 허용하지 않는 (유효한) 코드를 컴파일해 테스트하는 방법
11571정성태7/1/201819462.NET Framework: 782. C# - JIRA에 등록된 Project의 Version 항목 추가하는 방법파일 다운로드1
11570정성태7/1/201820197Math: 25. GeoGebra 기하 (2) - 임의의 선분과 특정 점을 지나는 수직선파일 다운로드1
11569정성태7/1/201819009Math: 24. GeoGebra 기하 (1) - 수직 이등분선파일 다운로드1
11568정성태7/1/201831184Math: 23. GeoGebra 기하 - 컴퍼스와 자를 이용한 작도 프로그램 [1]
11567정성태6/28/201820906.NET Framework: 781. C# - OpenCvSharp 사용 시 포인터를 이용한 속도 향상파일 다운로드1
... 91  92  93  94  [95]  96  97  98  99  100  101  102  103  104  105  ...