From 42dacf08ac81db998723f75282c2bb2095009f7a Mon Sep 17 00:00:00 2001
From: Philipp Feigl <philipp.feigl@cyan-it.de>
Date: Mon, 21 Oct 2024 10:23:40 +0200
Subject: [PATCH 1/3] Fix RuntimeConfigurationReader to support SelfContained
 builds

SelfContained builds to not contain the `frameworks` node in the `runtimeconfig.json` file.
They do however contain a `includedFrameworks` node. Details can be found here: https://github.com/dotnet/sdk/issues/3541

Running tests against a built assembly with the `SelfContained` option does result in the following often discussed error message
> Unable to instrument module
---
 coverlet.sln                                  |  7 +++++
 .../Instrumentation/CecilAssemblyResolver.cs  |  5 ++++
 .../WpfResolverTests.cs                       | 28 +++++++++++++++++++
 .../.editorconfig                             |  8 ++++++
 .../Program.cs                                |  9 ++++++
 .../TestClass.cs                              | 12 ++++++++
 ...ts.projectsample.wpf8.selfcontained.csproj | 14 ++++++++++
 .../Program.cs                                |  2 +-
 .../TestClass.cs                              |  2 +-
 9 files changed, 85 insertions(+), 2 deletions(-)
 create mode 100644 test/coverlet.tests.projectsample.wpf8.selfcontained/.editorconfig
 create mode 100644 test/coverlet.tests.projectsample.wpf8.selfcontained/Program.cs
 create mode 100644 test/coverlet.tests.projectsample.wpf8.selfcontained/TestClass.cs
 create mode 100644 test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj

diff --git a/coverlet.sln b/coverlet.sln
index 28fd22009..85241f5e4 100644
--- a/coverlet.sln
+++ b/coverlet.sln
@@ -88,6 +88,8 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "coverlet.tests.projectsampl
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.tests.utils", "test\coverlet.tests.utils\coverlet.tests.utils.csproj", "{0B109210-03CB-413F-888C-3023994AA384}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.tests.projectsample.wpf8.selfcontained", "test\coverlet.tests.projectsample.wpf8.selfcontained\coverlet.tests.projectsample.wpf8.selfcontained.csproj", "{71004336-9896-4AE5-8367-B29BB1680542}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -190,6 +192,10 @@ Global
 		{0B109210-03CB-413F-888C-3023994AA384}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{0B109210-03CB-413F-888C-3023994AA384}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{0B109210-03CB-413F-888C-3023994AA384}.Release|Any CPU.Build.0 = Release|Any CPU
+		{71004336-9896-4AE5-8367-B29BB1680542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{71004336-9896-4AE5-8367-B29BB1680542}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{71004336-9896-4AE5-8367-B29BB1680542}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{71004336-9896-4AE5-8367-B29BB1680542}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -220,6 +226,7 @@ Global
 		{351A034E-E642-4DB9-A21D-F71C8151C243} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
 		{03400776-1F9A-4326-B927-1CA9B64B42A1} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
 		{0B109210-03CB-413F-888C-3023994AA384} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
+		{71004336-9896-4AE5-8367-B29BB1680542} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {9CA57C02-97B0-4C38-A027-EA61E8741F10}
diff --git a/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs b/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs
index 7a6f1070a..0f384912b 100644
--- a/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs
+++ b/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs
@@ -318,6 +318,11 @@ public RuntimeConfigurationReader(string runtimeConfigFile)
         return runtimeOptionsElement["frameworks"].Select(x => (x["name"]?.Value<string>(), x["version"]?.Value<string>())).ToList();
       }
 
+      if (runtimeOptionsElement?["includedFrameworks"] != null)
+      {
+        return runtimeOptionsElement["includedFrameworks"].Select(x => (x["name"]?.Value<string>(), x["version"]?.Value<string>())).ToList();
+      }
+
       throw new InvalidOperationException($"Unable to read runtime configuration from {_runtimeConfigFile}.");
     }
   }
diff --git a/test/coverlet.integration.tests/WpfResolverTests.cs b/test/coverlet.integration.tests/WpfResolverTests.cs
index 23e7dc48b..0c1165279 100644
--- a/test/coverlet.integration.tests/WpfResolverTests.cs
+++ b/test/coverlet.integration.tests/WpfResolverTests.cs
@@ -43,5 +43,33 @@ public void TestInstrument_NetCoreSharedFrameworkResolver()
           "sample assembly shall be resolved");
       Assert.NotEmpty(assemblies);
     }
+
+    [ConditionalFact]
+    [SkipOnOS(OS.Linux, "WPF only runs on Windows")]
+    [SkipOnOS(OS.MacOS, "WPF only runs on Windows")]
+    public void TestInstrument_NetCoreSharedFrameworkResolver_SelfContained()
+    {
+      string buildConfiguration = TestUtils.GetAssemblyBuildConfiguration().ToString().ToLowerInvariant();
+      string wpfProjectPath = TestUtils.GetTestProjectPath("coverlet.tests.projectsample.wpf8.selfcontained");
+      string testBinaryPath = Path.Combine(TestUtils.GetTestBinaryPath("coverlet.tests.projectsample.wpf8.selfcontained"), buildConfiguration);
+      Assert.True(DotnetCli($"build \"{wpfProjectPath}\"", out string output, out string error));
+      string assemblyLocation = Directory.GetFiles(testBinaryPath, "coverlet.tests.projectsample.wpf8.selfcontained.dll", SearchOption.AllDirectories).First();
+
+      var mockLogger = new Mock<ILogger>();
+      var resolver = new NetCoreSharedFrameworkResolver(assemblyLocation, mockLogger.Object);
+      var compilationLibrary = new CompilationLibrary(
+          "package",
+          "System.Drawing",
+          "0.0.0.0",
+          "sha512-not-relevant",
+          Enumerable.Empty<string>(),
+          Enumerable.Empty<Dependency>(),
+          true);
+
+      var assemblies = new List<string>();
+      Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies),
+          "sample assembly shall be resolved");
+      Assert.NotEmpty(assemblies);
+    }
   }
 }
diff --git a/test/coverlet.tests.projectsample.wpf8.selfcontained/.editorconfig b/test/coverlet.tests.projectsample.wpf8.selfcontained/.editorconfig
new file mode 100644
index 000000000..d66ee0772
--- /dev/null
+++ b/test/coverlet.tests.projectsample.wpf8.selfcontained/.editorconfig
@@ -0,0 +1,8 @@
+# top-most EditorConfig file
+# We don't want to import other EditorConfig files and we want
+# to ensure no rules are enabled for these asset source files.
+root = true
+
+[*.cs]
+# Default severity for all analyzer diagnostics
+dotnet_analyzer_diagnostic.severity = none
diff --git a/test/coverlet.tests.projectsample.wpf8.selfcontained/Program.cs b/test/coverlet.tests.projectsample.wpf8.selfcontained/Program.cs
new file mode 100644
index 000000000..3eccb28c2
--- /dev/null
+++ b/test/coverlet.tests.projectsample.wpf8.selfcontained/Program.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace coverlet.tests.projectsample.wpf8.selfcontained;
+
+public static class Program
+{
+    public static void Main() { }
+}
diff --git a/test/coverlet.tests.projectsample.wpf8.selfcontained/TestClass.cs b/test/coverlet.tests.projectsample.wpf8.selfcontained/TestClass.cs
new file mode 100644
index 000000000..e86381aa1
--- /dev/null
+++ b/test/coverlet.tests.projectsample.wpf8.selfcontained/TestClass.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Windows.Controls;
+
+namespace coverlet.tests.projectsample.wpf8.selfcontained
+{
+    public class TestClass
+    {
+        public UserControl? Control { get; set; }
+    }
+}
diff --git a/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj b/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj
new file mode 100644
index 000000000..44e9580ce
--- /dev/null
+++ b/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj
@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net8.0-windows</TargetFramework>
+    <Nullable>enable</Nullable>
+    <UseWpf>true</UseWpf>
+    <IsTestProject>false</IsTestProject>
+    <EnableWindowsTargeting>true</EnableWindowsTargeting>
+    <IsPackable>false</IsPackable>
+	<SelfContained>true</SelfContained>
+  </PropertyGroup>
+
+</Project>
diff --git a/test/coverlet.tests.projectsample.wpf8/Program.cs b/test/coverlet.tests.projectsample.wpf8/Program.cs
index 205a96743..ba9c1bcd7 100644
--- a/test/coverlet.tests.projectsample.wpf8/Program.cs
+++ b/test/coverlet.tests.projectsample.wpf8/Program.cs
@@ -1,7 +1,7 @@
 // Copyright (c) Toni Solarin-Sodara
 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
-namespace coverlet.tests.projectsample.wpf6;
+namespace coverlet.tests.projectsample.wpf8;
 
 public static class Program
 {
diff --git a/test/coverlet.tests.projectsample.wpf8/TestClass.cs b/test/coverlet.tests.projectsample.wpf8/TestClass.cs
index 9299936c9..e38ac5467 100644
--- a/test/coverlet.tests.projectsample.wpf8/TestClass.cs
+++ b/test/coverlet.tests.projectsample.wpf8/TestClass.cs
@@ -3,7 +3,7 @@
 
 using System.Windows.Controls;
 
-namespace coverlet.tests.projectsample.wpf6
+namespace coverlet.tests.projectsample.wpf8
 {
     public class TestClass
     {

From bfbe67dd46e85fdc590e8a6df54c3c4816a7ae63 Mon Sep 17 00:00:00 2001
From: Philipp Feigl <philipp.feigl@cyan-it.de>
Date: Mon, 21 Oct 2024 10:57:42 +0200
Subject: [PATCH 2/3] Add RuntimeIdentifier to possibly fix build under mac and
 linux

---
 test/coverlet.integration.tests/WpfResolverTests.cs             | 2 +-
 .../coverlet.tests.projectsample.wpf8.selfcontained.csproj      | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/coverlet.integration.tests/WpfResolverTests.cs b/test/coverlet.integration.tests/WpfResolverTests.cs
index 0c1165279..779cb1e1b 100644
--- a/test/coverlet.integration.tests/WpfResolverTests.cs
+++ b/test/coverlet.integration.tests/WpfResolverTests.cs
@@ -51,7 +51,7 @@ public void TestInstrument_NetCoreSharedFrameworkResolver_SelfContained()
     {
       string buildConfiguration = TestUtils.GetAssemblyBuildConfiguration().ToString().ToLowerInvariant();
       string wpfProjectPath = TestUtils.GetTestProjectPath("coverlet.tests.projectsample.wpf8.selfcontained");
-      string testBinaryPath = Path.Combine(TestUtils.GetTestBinaryPath("coverlet.tests.projectsample.wpf8.selfcontained"), buildConfiguration);
+      string testBinaryPath = Path.Combine(TestUtils.GetTestBinaryPath("coverlet.tests.projectsample.wpf8.selfcontained"), $"{buildConfiguration}_win-x64");
       Assert.True(DotnetCli($"build \"{wpfProjectPath}\"", out string output, out string error));
       string assemblyLocation = Directory.GetFiles(testBinaryPath, "coverlet.tests.projectsample.wpf8.selfcontained.dll", SearchOption.AllDirectories).First();
 
diff --git a/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj b/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj
index 44e9580ce..1950b2244 100644
--- a/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj
+++ b/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj
@@ -9,6 +9,7 @@
     <EnableWindowsTargeting>true</EnableWindowsTargeting>
     <IsPackable>false</IsPackable>
 	<SelfContained>true</SelfContained>
+	<RuntimeIdentifier>win-x64</RuntimeIdentifier>
   </PropertyGroup>
 
 </Project>

From 2e9eea487eb0211d89769f1e14d9d302056689b5 Mon Sep 17 00:00:00 2001
From: Philipp Feigl <philipp.feigl@cyan-it.de>
Date: Mon, 21 Oct 2024 11:16:58 +0200
Subject: [PATCH 3/3] Tabs to spaces

---
 .../coverlet.tests.projectsample.wpf8.selfcontained.csproj    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj b/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj
index 1950b2244..662e3e604 100644
--- a/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj
+++ b/test/coverlet.tests.projectsample.wpf8.selfcontained/coverlet.tests.projectsample.wpf8.selfcontained.csproj
@@ -8,8 +8,8 @@
     <IsTestProject>false</IsTestProject>
     <EnableWindowsTargeting>true</EnableWindowsTargeting>
     <IsPackable>false</IsPackable>
-	<SelfContained>true</SelfContained>
-	<RuntimeIdentifier>win-x64</RuntimeIdentifier>
+    <SelfContained>true</SelfContained>
+    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
   </PropertyGroup>
 
 </Project>