Skip to content

Commit

Permalink
[System.Runtime.Loader] Add hot reload test infrastructure
Browse files Browse the repository at this point in the history
Adding infrastructure for hot reload testing.

For each test we define a new library assembly project.  The `.csproj` has a
`DeltaScript` property that specifies a JSON file that lists the name of an
initial source file, and a list of updated versions of that file.  The
`hotreload-delta-gen` tool runs during the build to read the delta script and
create deltas that incorporate the updates.

The main testsuite references all the test assemblies, and when a test runs, it
calls `ApplyUpdateUtil.ApplyUpdate` to load subsequent deltas
and then compares the results before and after an update.

Dependencies:

- https://github.com/dotnet/hotreload-utils  the `hotreload-delta-gen` binary
must be installed and on the PATH  (TODO: package it as a dotnet tool and
publish to a transport nuget package)

Needs work:

- Mono test runs need to pass the `FEATURE_MONO_METADATA_UPDATE` property to
  msbuild to the test project.  This is because not every mono configuration
  includes hot reload support.  Eventually this should be subsumed by the
  runtime API to querty hot reload capabilities dotnet#50111
- All runs need to pass `DOTNET_MODIFIABLE_ASSEMBLIES=debug` to the testsuite.
  Hot reload only works when:
  - the assemblies are compiled with the Debug compiler setting,
  - and, if the runtime is tarted with the above environment variable set.
- The GenerateHotReloadDeltas.targets needs some output file work to run `hotreload-delta-gen` less
  frequently.  Right now it runs every time even if everything is up to date.
  For CI testing we should ideally compile the deltas ahead of time once.

To try it out locally:

1. checkout and build `hotreload-utils` and do `dotnet publish --self-contained -r <RID>` to
put `hotreload-delta-gen` into that projects' `artifacts/...` forlder.  Add the
publish folder to your `$PATH`

2. In dotnet/runtime run
```
DOTNET_MODIFIABLE_ASSEMBLIES=debug MONO_ENV_OPTIONS=--interp ./dotnet.sh build src/libraries/System.Runtime.Loader/tests /p:MonoMetadataUpdate=true /t:Test
```

(CoreCLR doesn't need `MONO_ENV_OPTIONS` or `MonoMetadataUpdate`)
  • Loading branch information
lambdageek committed May 10, 2021
1 parent 835f7c9 commit a29f09c
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<Import Project="..\..\Directory.Build.props" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<Import Project="..\..\..\Directory.Build.targets" />

<Import Project="..\GenerateHotReloadDelta.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

namespace System.Reflection.Metadata.ApplyUpdate.Test
{
public class MethodBody1 {
public static string StaticMethod1 () {
return "OLD STRING";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

namespace System.Reflection.Metadata.ApplyUpdate.Test
{
public class MethodBody1 {
public static string StaticMethod1 () {
return "NEW STRING";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

namespace System.Reflection.Metadata.ApplyUpdate.Test
{
public class MethodBody1 {
public static string StaticMethod1 () {
return "NEWEST STRING";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>System.Runtime.Loader.Tests</RootNamespace>
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
<TestRuntime>true</TestRuntime>
<!-- Mono doesn't support hot reload in every configuration. -->
<DefineConstants Condition="'$(MonoMetadataUpdate)' == 'true'">FEATURE_MONO_APPLY_UPDATE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="MethodBody1.cs" />
</ItemGroup>

<PropertyGroup>
<DeltaScript>deltascript.json</DeltaScript>
<DeltaCount>2</DeltaCount>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": [
{"document": "MethodBody1.cs", "update": "MethodBody1_v1.cs"},
{"document": "MethodBody1.cs", "update": "MethodBody1_v2.cs"},
]
}

39 changes: 39 additions & 0 deletions src/libraries/System.Runtime.Loader/tests/ApplyUpdateTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace System.Reflection.Metadata
{
///
/// The general setup for ApplyUpdate tests is:
///
/// Each test Foo has a corresponding assembly under
/// System.Reflection.Metadata.ApplyUpate.Test.Foo The Foo.csproj has a delta
/// script that applies one or more updates to Foo.dll The ApplyUpdateTest
/// testusuite runs each test in sequence, loading the corresponding
/// assembly, applying an update to it and observing the results.
[Collection(nameof(NoParallelTests))]
[ConditionalClass(typeof(ApplyUpdateUtil), nameof (ApplyUpdateUtil.IsSupported))]
public class ApplyUpdateTest
{

[Fact]
void StaticMethodBodyUpdate()
{
var r = ApplyUpdate.Test.MethodBody1.StaticMethod1 ();
Assert.Equal ("OLD STRING", r);

ApplyUpdateUtil.ApplyUpdate(typeof (ApplyUpdate.Test.MethodBody1).Assembly);

r = ApplyUpdate.Test.MethodBody1.StaticMethod1 ();
Assert.Equal ("NEW STRING", r);

ApplyUpdateUtil.ApplyUpdate(typeof (ApplyUpdate.Test.MethodBody1).Assembly);

r = ApplyUpdate.Test.MethodBody1.StaticMethod1 ();
Assert.Equal ("NEWEST STRING", r);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<Project>
<!-- Invoke hotreload-delta-gen to apply the DeltaScript to the output assembly to
create the .dmeta, .dil and .dpdb files on the OutputPath -->
<!-- Have to be careful about AfterTargets. If we happen to go after a design-time target, we could get into an infinite loop. -->
<Target Name="CompileDiff" AfterTargets="Build" Condition="'$(DeltaScript)' != ''">
<Message Importance="High" Condition="'$(HotreloadDeltaGenFullPath)' == ''" Text="WARNING: HotreloadDeltaGenFullPath property is unset. Will run 'hotreload-delta-gen' assuming it is on the PATH" />
<PropertyGroup>
<HotreloadDeltaGenFullPath Condition="'$(HotreloadDeltaGenFullPath)' == ''">hotreload-delta-gen</HotreloadDeltaGenFullPath>
<HotreloadDeltaGenArgs>-msbuild:$(MSBuildProjectFullPath)</HotreloadDeltaGenArgs>
<!-- HACK: have to pass config and RID so that this target works for a 'dotnet publish' run.
What other properties do I need to pass? Maybe hotreload-delta-gen should just expose an MSBuild task so we can pass everything -->
<HotreloadDeltaGenArgs Condition="'$(Configuration)' != ''">$(HotreloadDeltaGenArgs) -p:Configuration=$(Configuration)</HotreloadDeltaGenArgs>
<HotreloadDeltaGenArgs Condition="'$(RuntimeIdentifier)' != ''">$(HotreloadDeltaGenArgs) -p:RuntimeIdentifier=$(RuntimeIdentifier)</HotreloadDeltaGenArgs>
<HotreloadDeltaGenArgs Condition="'$(BuiltRuntimeConfiguration)' != ''">$(HotreloadDeltaGenArgs) -p:BuiltRuntimeConfiguration=$(BuiltRuntimeConfiguration)</HotreloadDeltaGenArgs>
<HotreloadDeltaGenArgs>$(HotreloadDeltaGenArgs) -script:$(DeltaScript)</HotreloadDeltaGenArgs>
</PropertyGroup>
<Exec Command="$(HotreloadDeltaGenFullPath) $(HotreloadDeltaGenArgs)"/>
</Target>

<!-- Computes the names of the files that hotreload-delta-gen will produce, given the name of the base assembly and the
number of deltas -->
<UsingTask TaskName="ComputeDeltaFileOutputNames" TaskFactory="RoslynCodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
<ParameterGroup>
<BaseAssemblyName ParameterType="System.String" Required="true" />
<DeltaCount ParameterType="System.Int32" Required="true" />
<DeltaOutputs ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
</ParameterGroup>
<Task>
<!-- <Reference Include=""/> -->
<Using Namespace="System"/>
<Using Namespace="System.IO"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
// Display "Hello, world!"
//Log.LogWarning("Hello, world!");
// Log.LogMessageFromText($"Parameter1: '{Parameter1}'", MessageImportance.High);
int count = DeltaCount;
if (count == 0) {
Log.LogError("Did not expect 0 deltas");
Success = false;
return Success;
}
string baseAssemblyName = BaseAssemblyName;
ITaskItem[] result = new TaskItem[3*count];
for (int i = 0; i < count; ++i) {
int rev = 1+i;
string dmeta = baseAssemblyName + $".{rev}.dmeta";
string dil = baseAssemblyName + $".{rev}.dil";
string dpdb = baseAssemblyName + $".{rev}.dpdb";
result[3*i] = new TaskItem(dmeta);
result[3*i+1] = new TaskItem(dil);
result[3*i+2] = new TaskItem(dpdb);
}
DeltaOutputs = result;
]]>
</Code>
</Task>
</UsingTask>

<Target Name="ComputeDeltaFileOutputNames">
<Error Condition="'$(DeltaCount)' == ''" Text="Set the DeltaCount property to the total number of deltas in this project" />
<!-- FIXME: is AssemblyName the best property to get the output assembly? -->
<ComputeDeltaFileOutputNames BaseAssemblyName="$(OutputPath)\$(AssemblyName).dll" DeltaCount="$(DeltaCount)">
<Output TaskParameter="DeltaOutputs" ItemName="_DeltaFileForAssignTargetPaths" />
</ComputeDeltaFileOutputNames>
</Target>

<Target Name="ContentForDeltaFileOutputNames"
DependsOnTargets="ComputeDeltaFileOutputNames"
BeforeTargets="AssignTargetPaths"
Condition="'$(DesignTimeBuild)' != 'true' and '$(BuildingProject)' == 'true'"
>
<ItemGroup>
<Content Include="@(_DeltaFileForAssignTargetPaths)">
<CopyToOutputDirectory>always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Target>

</Project>
74 changes: 74 additions & 0 deletions src/libraries/System.Runtime.Loader/tests/Helpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Xunit;

namespace System.Reflection.Metadata
{

[CollectionDefinition("NoParallelTests", DisableParallelization = true)]
public partial class NoParallelTests { }

public class ApplyUpdateUtil {
// FIXME: Use runtime API https://github.com/dotnet/runtime/issues/50111 when it is approved/implemented
public static bool IsSupported => IsModifiableAssembliesSet &&
(!IsMonoRuntime || IsSupportedMonoConfiguration()) &&
IsSupportedTestConfiguration();

public static bool IsModifiableAssembliesSet =>
String.Equals("debug", Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES"), StringComparison.InvariantCultureIgnoreCase);

// copied from https://github.com/dotnet/arcade/blob/6cc4c1e9e23d5e65e88a8a57216b3d91e9b3d8db/src/Microsoft.DotNet.XUnitExtensions/src/DiscovererHelpers.cs#L16-L17
private static readonly Lazy<bool> s_isMonoRuntime = new Lazy<bool>(() => Type.GetType("Mono.RuntimeStructs") != null);
public static bool IsMonoRuntime => s_isMonoRuntime.Value;

// Not every build of Mono supports ApplyUpdate
internal static bool IsSupportedMonoConfiguration()
{
#if FEATURE_MONO_APPLY_UPDATE
// crude check for interp mode
return System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported && !System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled;
#else
return false;
#endif
}


// Only Debug assemblies are editable
internal static bool IsSupportedTestConfiguration()
{
#if DEBUG
return true;
#else
return false;
#endif
}

private static System.Collections.Generic.Dictionary<Assembly, int> assembly_count = new ();

public static void ApplyUpdate (System.Reflection.Assembly assm)
{
int count;
if (!assembly_count.TryGetValue(assm, out count))
count = 1;
else
count++;
assembly_count [assm] = count;

/* FIXME WASM: Location is empty on wasm. Make up a name based on Name */
string basename = assm.Location;
if (basename == "")
basename = assm.GetName().Name + ".dll";
Console.WriteLine ($"Apply Delta Update for {basename}, revision {count}");

string dmeta_name = $"{basename}.{count}.dmeta";
string dil_name = $"{basename}.{count}.dil";
byte[] dmeta_data = System.IO.File.ReadAllBytes (dmeta_name);
byte[] dil_data = System.IO.File.ReadAllBytes (dil_name);
byte[] dpdb_data = null; // TODO also use the dpdb data

AssemblyExtensions.ApplyUpdate(assm, dmeta_data, dil_data, dpdb_data);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<!-- Some tests rely on no deps.json file being present. -->
<GenerateDependencyFile>false</GenerateDependencyFile>
<!-- Mono doesn't support hot reload in every configuration. -->
<DefineConstants Condition="'$(MonoMetadataUpdate)' == 'true'">FEATURE_MONO_APPLY_UPDATE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="ApplyUpdateTest.cs" />
<Compile Include="AssemblyExtensionsTest.cs" />
<Compile Include="AssemblyLoadContextTest.cs" />
<Compile Include="CollectibleAssemblyLoadContextTest.cs" />
<Compile Include="ContextualReflection.cs" />
<Compile Include="CustomTPALoadContext.cs" />
<Compile Include="MetadataUpdateHandlerAttributeTest.cs" />
<Compile Include="Helpers.cs" />
<Compile Include="ResourceAssemblyLoadContext.cs" />
<Compile Include="SatelliteAssemblies.cs" />
<Compile Include="LoaderLinkTest.cs" />
Expand All @@ -34,6 +38,7 @@
<ProjectReference Include="ReferencedClassLibNeutralIsSatellite\ReferencedClassLibNeutralIsSatellite.csproj" />
<ProjectReference Include="LoaderLinkTest.Shared\LoaderLinkTest.Shared.csproj" />
<ProjectReference Include="LoaderLinkTest.Dynamic\LoaderLinkTest.Dynamic.csproj" />
<ProjectReference Include="ApplyUpdate\System.Reflection.Metadata.ApplyUpdate.Test.MethodBody1\System.Reflection.Metadata.ApplyUpdate.Test.MethodBody1.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetOS)' == 'Browser'">
<WasmFilesToIncludeFromPublishDir Include="$(AssemblyName).dll" />
Expand Down

0 comments on commit a29f09c

Please sign in to comment.