Add option for building a test exe as single file (#42972)
authorAndy Gocke <angocke@microsoft.com>
Wed, 31 Mar 2021 18:34:05 +0000 (11:34 -0700)
committerGitHub <noreply@github.com>
Wed, 31 Mar 2021 18:34:05 +0000 (18:34 +0000)
* Add option for building a test exe as single file

* Remove left over test

* Add target to exclude references from single-file

* First attempt at adding a CI job

* Opt-in specific libraries for single-file testing support

Start with System.Collections as all tests pass.

* Config testing using single-file in build.cmd

* Change yml suffix name to SingleFile

* Windows_NT_x64 -> windows_x64

* Fix for helix queueing

* Respond to host rename

* Change TargetOS to check for windows

* chmod test exe on linux

* Direct singlefilehost to the locally built copy

* Adjust singlefilehost copy+call

* Add .exe suffix on Windows

* Move libraries after hosts build to allow for libs.test to depend on hosts build

* Split up host and libs packaging and tests

* Move host packaging

* Move pretest up

* Move packages up as well

* Reorder libs pretest and libs.packages

* Add isSingleFile build parameter to limit Linux Helix jobs

* Typo

* Change conditional check

* Fix yml

* Fix yml

* Fix neq

* Fix subsets

* Typo

* Fix

* Adjust assert

* Include code from Michal to skip failing test

* Remove empty ItemGroup

* Update src/libraries/Common/tests/SingleFileTestRunner/SingleFileTestRunner.cs

Co-authored-by: Michal Strehovský <MichalStrehovsky@users.noreply.github.com>
* Update eng/testing/tests.singlefile.targets

Co-authored-by: Michal Strehovský <MichalStrehovsky@users.noreply.github.com>
* Apply suggestions from code review

Co-authored-by: Michal Strehovský <MichalStrehovsky@users.noreply.github.com>
* Use ProjectExclusions

* Update eng/testing/tests.singlefile.targets

Co-authored-by: Michal Strehovský <MichalStrehovsky@users.noreply.github.com>
* Revert changes

* Remove host build from tests

Co-authored-by: Michal Strehovský <MichalStrehovsky@users.noreply.github.com>
eng/pipelines/libraries/helix-queues-setup.yml
eng/pipelines/runtime.yml
eng/testing/tests.singlefile.targets [new file with mode: 0644]
eng/testing/tests.targets
eng/testing/xunit/xunit.console.targets
eng/testing/xunit/xunit.props
src/libraries/Common/tests/SingleFileTestRunner/SingleFileTestRunner.cs [new file with mode: 0644]
src/libraries/System.Reflection/tests/System.Reflection.Tests.csproj
src/libraries/tests.proj

index 8b0f588..90bf48d 100644 (file)
@@ -52,7 +52,7 @@ jobs:
 
     # Linux x64
     - ${{ if eq(parameters.platform, 'Linux_x64') }}:
-      - ${{ if eq(parameters.jobParameters.interpreter, '') }}:
+      - ${{ if and(eq(parameters.jobParameters.interpreter, ''), ne(parameters.jobParameters.isSingleFile, true)) }}:
         - ${{ if and(eq(parameters.jobParameters.testScope, 'outerloop'), eq(parameters.jobParameters.runtimeFlavor, 'mono')) }}:
           - (Centos.8.Amd64.Open)Ubuntu.1604.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:centos-8-helix-20201229003624-c1bf759
           - RedHat.7.Amd64.Open
@@ -81,7 +81,7 @@ jobs:
             - Ubuntu.1804.Amd64.Open
             - SLES.15.Amd64.Open
             - (Fedora.30.Amd64.Open)ubuntu.1604.amd64.open@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-30-helix-20200512010621-4f8cef7
-      - ${{ if eq(parameters.jobParameters.interpreter, 'true') }}:
+      - ${{ if or(eq(parameters.jobParameters.interpreter, 'true'), eq(parameters.jobParameters.isSingleFile, true)) }}:
         # Limiting interp runs as we don't need as much coverage.
         - Debian.9.Amd64.Open
 
@@ -135,7 +135,7 @@ jobs:
       # .NETFramework
       - ${{ if eq(parameters.jobParameters.framework, 'net48') }}:
         - Windows.10.Amd64.Client19H1.Open
-      
+
       # AllConfigurations
       - ${{ if eq(parameters.jobParameters.framework, 'allConfigurations') }}:
         - Windows.10.Amd64.Server19H1.Open
index df3f7ca..476150a 100644 (file)
@@ -302,6 +302,28 @@ jobs:
           eq(variables['monoContainsChange'], true),
           eq(variables['isFullMatrix'], true))
 
+# Build and test libraries under single-file publishing
+- template: /eng/pipelines/common/platform-matrix.yml
+  parameters:
+    jobTemplate: /eng/pipelines/common/global-build-job.yml
+    helixQueuesTemplate: /eng/pipelines/libraries/helix-queues-setup.yml
+    buildConfig: Release
+    platforms:
+    - windows_x64
+    - Linux_x64
+    jobParameters:
+      testGroup: innerloop
+      isFullMatrix: ${{ variables.isFullMatrix }}
+      isSingleFile: true
+      nameSuffix: SingleFile
+      buildArgs: -s clr+libs+libs.tests -c $(_BuildConfig) /p:TestSingleFile=true /p:ArchiveTests=true
+      timeoutInMinutes: 120
+      # extra steps, run tests
+      extraStepsTemplate: /eng/pipelines/libraries/helix.yml
+      extraStepsParameters:
+        creator: dotnet-bot
+        testRunNamePrefixSuffix: SingleFile_$(_BuildConfig)
+
 #
 # Build the whole product using Mono and run runtime tests
 #
diff --git a/eng/testing/tests.singlefile.targets b/eng/testing/tests.singlefile.targets
new file mode 100644 (file)
index 0000000..86cbc7e
--- /dev/null
@@ -0,0 +1,62 @@
+<Project>
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+
+    <BundleDir>$([MSBuild]::NormalizeDirectory('$(OutDir)', 'publish'))</BundleDir>
+    <RunScriptOutputPath>$([MSBuild]::NormalizePath('$(BundleDir)', '$(RunScriptOutputName)'))</RunScriptOutputPath>
+    <RuntimeIdentifier>$(PackageRID)</RuntimeIdentifier>
+
+    <RunScriptCommand Condition="'$(TargetOS)' == 'windows'">$(AssemblyName).exe</RunScriptCommand>
+    <RunScriptCommand Condition="'$(TargetOS)' != 'windows'">chmod +rwx $(AssemblyName) &amp;&amp; ./$(AssemblyName)</RunScriptCommand>
+  </PropertyGroup>
+  
+  <PropertyGroup>
+    <PublishSingleFile>true</PublishSingleFile>
+    <UseAppHost>true</UseAppHost>
+    <SelfContained>true</SelfContained>
+    <SingleFileHostSourcePath>$([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'coreclr', '$(TargetOS).$(TargetArchitecture).$(Configuration)', 'corehost'))/singlefilehost</SingleFileHostSourcePath>
+    <SingleFileHostSourcePath Condition="'$(TargetOS)' == 'windows'">$(SingleFileHostSourcePath).exe</SingleFileHostSourcePath>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="$(CommonTestPath)SingleFileTestRunner\SingleFileTestRunner.cs"
+             Link="Common\SingleFileTestRunner\SingleFileTestRunner.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="xunit.runner.utility" Version="$(XUnitVersion)" />
+  </ItemGroup>
+
+  <Target Name="__ExcludeAssembliesFromSingleFile"
+          Inputs="%(ResolvedFileToPublish.Identity)"
+          Outputs="__NewResolvedFiles"
+          BeforeTargets="_ComputeFilesToBundle">
+    <PropertyGroup>
+      <__Identity>%(ResolvedFileToPublish.Identity)</__Identity>
+      <__FileName>%(ResolvedFileToPublish.Filename)%(ResolvedFileToPublish.Extension)</__FileName>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <__NewResolvedFiles Include="@(ResolvedFileToPublish)">
+        <ExcludeFromSingleFile Condition="'%(__ExcludeFromBundle.Identity)' == '$(__FileName)'">true</ExcludeFromSingleFile>
+      </__NewResolvedFiles>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="__UpdateExcludedAssembliesFromSingleFile"
+          Inputs="ExcludeFromSingleFile"
+          Outputs="ResolvedFileToPublish"
+          DependsOnTargets="ComputeResolvedFilesToPublishList"
+          BeforeTargets="_ComputeFilesToBundle">
+    <ItemGroup>
+      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" />
+      <ResolvedFileToPublish Include="@(__NewResolvedFiles)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="PublishTestAsSingleFile"
+          Condition="'$(IsCrossTargetingBuild)' != 'true'"
+          AfterTargets="Build"
+          DependsOnTargets="Publish;ArchiveTests" />
+
+</Project>
index b7b642b..9fc08dd 100644 (file)
@@ -25,7 +25,7 @@
     <!-- For browser we need to hook up the target with DependsOnTargets in PublishTestAsSelfContained
     because we do a Publish which runs after Build, if we run after PrepareForRun we would generated
     an empty zip because we haven't published the selfcontained app.  -->
-    <ArchiveTestsAfterTargets Condition="'$(TargetOS)' == 'Browser'" />
+    <ArchiveTestsAfterTargets Condition="'$(TargetOS)' == 'Browser' or '$(TestSingleFile)' == 'true'" />
   </PropertyGroup>
 
   <!-- Archive test binaries. -->
@@ -37,7 +37,7 @@
 
     <PropertyGroup>
       <_ZipSourceDirectory>$(OutDir)</_ZipSourceDirectory>
-      <_ZipSourceDirectory Condition="'$(TargetOS)' == 'Browser'">$(BundleDir)</_ZipSourceDirectory>
+      <_ZipSourceDirectory Condition="'$(TargetOS)' == 'Browser' or '$(TestSingleFile)' == 'true'">$(BundleDir)</_ZipSourceDirectory>
     </PropertyGroup>
 
     <MakeDir Directories="$(TestArchiveTestsDir)" />
   </Target>
 
   <Import Project="$(MSBuildThisFileDirectory)tests.mobile.targets" Condition="'$(TargetsMobile)' == 'true'" />
+  <Import Project="$(MSBuildThisFileDirectory)tests.singlefile.targets" Condition="'$(TestSingleFile)' == 'true'" />
   <Import Project="$(MSBuildThisFileDirectory)xunit\xunit.targets" Condition="'$(TestFramework)' == 'xunit'" />
 
   <!-- Main test targets -->
   <Target Name="Test" DependsOnTargets="$(TestDependsOn)" />
-  
+
   <Import Project="$(MSBuildThisFileDirectory)outerBuild.targets" Condition="'$(IsCrossTargetingBuild)' == 'true'" />
 </Project>
index a88b08f..7710a05 100644 (file)
@@ -5,7 +5,7 @@
     <UseXunitExcludesTxtFile Condition="'$(TargetOS)' == 'Android' or '$(TargetOS)' == 'iOS' or '$(TargetOS)' == 'iOSSimulator' or '$(TargetOS)' == 'tvOS' or '$(TargetOS)' == 'tvOSSimulator'">true</UseXunitExcludesTxtFile>
   </PropertyGroup>
 
-  <PropertyGroup Condition="'$(TargetsMobile)' != 'true'">
+  <PropertyGroup Condition="'$(TargetsMobile)' != 'true' and '$(TestSingleFile)' != 'true'">
     <_depsFileArgument Condition="'$(GenerateDependencyFile)' == 'true'">--depsfile $(AssemblyName).deps.json</_depsFileArgument>
     <RunScriptCommand Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">"$(RunScriptHost)" exec --runtimeconfig $(AssemblyName).runtimeconfig.json $(_depsFileArgument) xunit.console.dll</RunScriptCommand>
     <RunScriptCommand Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">xunit.console.exe</RunScriptCommand>
@@ -36,7 +36,7 @@
     <XunitExcludesTxtFileContent>$(_withoutCategories.Replace(';', '%0dcategory='))</XunitExcludesTxtFileContent>
   </PropertyGroup>
 
-  <ItemGroup>
+  <ItemGroup Condition="'$(TestSingleFile)' != 'true'">
     <PackageReference Include="Microsoft.DotNet.XUnitConsoleRunner"
                       Version="$(MicrosoftDotNetXUnitConsoleRunnerVersion)"
                       Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
index f63b390..3f9c4b6 100644 (file)
@@ -13,7 +13,7 @@
     <PackageReference Include="Microsoft.DotNet.XUnitExtensions" Version="$(MicrosoftDotNetXUnitExtensionsVersion)" />
   </ItemGroup>
 
-  <ItemGroup Condition="'$(ArchiveTests)' != 'true' AND '$(PublishingTestsRun)' != 'true'">
+  <ItemGroup Condition="'$(ArchiveTests)' != 'true' and '$(PublishingTestsRun)' != 'true' and '$(TestSingleFile)' != 'true'">
     <!-- Microsoft.Net.Test.Sdk brings a lot of assemblies with it. To reduce helix payload submission size we disable it on CI. -->
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkVersion)" />
     <PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)" GeneratePathProperty="true" />
diff --git a/src/libraries/Common/tests/SingleFileTestRunner/SingleFileTestRunner.cs b/src/libraries/Common/tests/SingleFileTestRunner/SingleFileTestRunner.cs
new file mode 100644 (file)
index 0000000..7b67a13
--- /dev/null
@@ -0,0 +1,77 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+public class SingleFileTestRunner : XunitTestFramework
+{
+    private SingleFileTestRunner(IMessageSink messageSink)
+    : base(messageSink) { }
+
+    public static int Main(string[] args)
+    {
+        var asm = typeof(SingleFileTestRunner).Assembly;
+        Console.WriteLine("Running assembly:" + asm.FullName);
+
+        var diagnosticSink = new ConsoleDiagnosticMessageSink();
+        var testsFinished = new TaskCompletionSource();
+        var testSink = new TestMessageSink();
+        var summarySink = new DelegatingExecutionSummarySink(testSink,
+            () => false,
+            (completed, summary) => Console.WriteLine($"Tests run: {summary.Total}, Errors: {summary.Errors}, Failures: {summary.Failed}, Skipped: {summary.Skipped}. Time: {TimeSpan.FromSeconds((double)summary.Time).TotalSeconds}s"));
+        var resultsXmlAssembly = new XElement("assembly");
+        var resultsSink = new DelegatingXmlCreationSink(summarySink, resultsXmlAssembly);
+
+        testSink.Execution.TestSkippedEvent += args => { Console.WriteLine($"[SKIP] {args.Message.Test.DisplayName}"); };
+        testSink.Execution.TestFailedEvent += args => { Console.WriteLine($"[FAIL] {args.Message.Test.DisplayName}{Environment.NewLine}{Xunit.ExceptionUtility.CombineMessages(args.Message)}{Environment.NewLine}{Xunit.ExceptionUtility.CombineStackTraces(args.Message)}"); };
+
+        testSink.Execution.TestAssemblyFinishedEvent += args =>
+        {
+            Console.WriteLine($"Finished {args.Message.TestAssembly.Assembly}{Environment.NewLine}");
+            testsFinished.SetResult();
+        };
+
+        var xunitTestFx = new SingleFileTestRunner(diagnosticSink);
+        var asmInfo = Reflector.Wrap(asm);
+        var asmName = asm.GetName();
+
+        var discoverySink = new TestDiscoverySink();
+        var discoverer = xunitTestFx.CreateDiscoverer(asmInfo);
+        discoverer.Find(false, discoverySink, TestFrameworkOptions.ForDiscovery());
+        discoverySink.Finished.WaitOne();
+        XunitFilters filters = new XunitFilters();
+        filters.ExcludedTraits.Add("category", new List<string> { "failing" });
+        var filteredTestCases = discoverySink.TestCases.Where(filters.Filter).ToList();
+        var executor = xunitTestFx.CreateExecutor(asmName);
+        executor.RunTests(filteredTestCases, resultsSink, TestFrameworkOptions.ForExecution());
+
+        resultsSink.Finished.WaitOne();
+
+        var failed = resultsSink.ExecutionSummary.Failed > 0 || resultsSink.ExecutionSummary.Errors > 0;
+        return failed ? 1 : 0;
+    }
+}
+
+internal class ConsoleDiagnosticMessageSink : IMessageSink
+{
+    public bool OnMessage(IMessageSinkMessage message)
+    {
+        if (message is IDiagnosticMessage diagnosticMessage)
+        {
+            return true;
+        }
+        return false;
+    }
+}
index 5b4ff27..deceb65 100644 (file)
@@ -71,4 +71,8 @@
     <WasmFilesToIncludeFromPublishDir Include="$(AssemblyName).dll" />
     <WasmFilesToIncludeFromPublishDir Include="$(AssemblyName).pdb" />
   </ItemGroup>
+  <ItemGroup>
+    <!-- Assemblies that should be excluded from the bundle -->
+    <__ExcludeFromBundle Include="TestAssembly.dll" />
+  </ItemGroup>
 </Project>
index 55fdbc1..1442c9d 100644 (file)
@@ -12,7 +12,7 @@
     <TestPackages Condition="'$(TestPackages)' == ''">false</TestPackages>
     <TestTrimming Condition="'$(TestTrimming)' == ''">false</TestTrimming>
   </PropertyGroup>
-  
+
   <!-- Projects that don't support code coverage measurement. -->
   <ItemGroup Condition="'$(Coverage)' == 'true'">
     <ProjectExclusions Include="$(CommonTestPath)Common.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.NameResolution\tests\UnitTests\System.Net.NameResolution.Unit.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.NetworkInformation\tests\FunctionalTests\System.Net.NetworkInformation.Functional.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.Ping\tests\FunctionalTests\System.Net.Ping.Functional.Tests.csproj" />
-    
+
     <!-- https://github.com/dotnet/runtime/issues/49191 -->
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.Quic\tests\FunctionalTests\System.Net.Quic.Functional.Tests.csproj" />
-    
+
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.Requests\tests\System.Net.Requests.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.Security\tests\FunctionalTests\System.Net.Security.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.Sockets\tests\FunctionalTests\System.Net.Sockets.Tests.csproj" />
-    
+
     <!-- https://github.com/dotnet/runtime/issues/49192-->
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.WebClient\tests\System.Net.WebClient.Tests.csproj" />
-    
+
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.WebSockets\tests\System.Net.WebSockets.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Net.WebSockets.Client\tests\System.Net.WebSockets.Client.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Numerics.Tensors\tests\System.Numerics.Tensors.Tests.csproj" />
     <ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Threading.Timer/tests/System.Threading.Timer.Tests.csproj" />
   </ItemGroup>
 
+  <ItemGroup Condition="'$(TestSingleFile)' == 'true'">
+    <!-- Run only a small randomly chosen set of passing test suites -->
+    <ProjectExclusions Include="$(MSBuildThisFileDirectory)*\tests\**\*.Tests.csproj" />
+    <ProjectExclusions Remove="$(MSBuildThisFileDirectory)System.Collections\tests\System.Collections.Tests.csproj" />
+  </ItemGroup>
+
   <ItemGroup>
     <ProjectReference Include="$(MSBuildThisFileDirectory)*\tests\**\*.Tests.csproj"
                       Exclude="@(ProjectExclusions)"
     <ProjectReference Include="$(RepoRoot)\src\tests\FunctionalTests\tvOS\**\*.Test.csproj"
                       BuildInParallel="false" />
   </ItemGroup>
-  
+
   <ItemGroup Condition="'$(ArchiveTests)' == 'true' and '$(TargetOS)' == 'Android'">
     <ProjectReference Include="$(MonoProjectRoot)sample\Android\AndroidSampleApp.csproj"
                       BuildInParallel="false" />
                       Exclude="$(RepoRoot)\src\tests\FunctionalTests\Android\Device_Emulator\AOT\Android.Device_Emulator.Aot.Test.csproj"
                       BuildInParallel="false" />
   </ItemGroup>
-  
+
   <ItemGroup Condition="'$(ArchiveTests)' == 'true' and '$(TargetOS)' == 'Browser'">
     <ProjectReference Include="$(MonoProjectRoot)sample\wasm\**\*.Sample.csproj"
                       BuildInParallel="false" />