Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assembly load/unload tests and fixes #138

Merged
merged 13 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ jobs:
if: startsWith (matrix.os, 'windows') == false
# dotnet test doesn't support mono so we have to use the xunit console runner
run: |
nuget install xunit.runner.console -Version 2.4.1 -OutputDirectory testrunner
mono ./testrunner/xunit.runner.console.2.4.1/tools/net472/xunit.console.exe ./Mono.TextTemplating.Tests/bin/${{ matrix.config }}/net472/Mono.TextTemplating.Tests.dll -parallel none -noshadow -noappdomain
nuget install xunit.runner.console -Version 2.4.2 -OutputDirectory testrunner
mono ./testrunner/xunit.runner.console.2.4.2/tools/net472/xunit.console.exe ./Mono.TextTemplating.Tests/bin/${{ matrix.config }}/net472/Mono.TextTemplating.Tests.dll -parallel none -noshadow
dotnet test -c ${{ matrix.config }} --no-build -f netcoreapp2.1
dotnet test -c ${{ matrix.config }} --no-build -f netcoreapp3.1
dotnet test -c ${{ matrix.config }} --no-build -f net5.0
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
packages/
TestResults/
.vs/
testrunner

# globs
*.DS_Store
Expand Down
5 changes: 0 additions & 5 deletions Mono.TextTemplating.Roslyn/Mono.TextTemplating.Roslyn.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ To enable the in-process C# compiler, use the TemplatingEngine.UseInProcessCompi
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>


<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" />
</ItemGroup>
Expand Down
126 changes: 126 additions & 0 deletions Mono.TextTemplating.Tests/AppDomainTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if FEATURE_APPDOMAINS

using System;
using System.CodeDom.Compiler;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

using Xunit;

namespace Mono.TextTemplating.Tests;

public class AppDomainTests : AssemblyLoadTests<SnapshotSet<string>>
{
protected override TemplateGenerator CreateGenerator ([CallerMemberName] string testName = null) => CreateGeneratorWithAppDomain (testName: testName);

protected override void CleanupGenerator (TemplateGenerator generator)
{
// FIXME: app domain unload doesn't seem to work on Mono
if (FactExceptOnMonoAttribute.IsRunningOnMono) {
return;
}

// verify that the AppDomain is collected
var weakRef = ((TestTemplateGeneratorWithAppDomain)generator).ReleaseDomain ();
int count = 0;
while (weakRef.IsAlive) {
GC.Collect ();
Assert.InRange (count++, 0, 5);
}
}

protected override SnapshotSet<string> GetInitialState () => Snapshot.LoadedAssemblies ();

protected override void VerifyFinalState (SnapshotSet<string> state)
{
(var added, var removed) = state.GetChanges ();
added = added.Where (a => !ShouldIgnoreAssemblyAdd (a));
Assert.Empty (added);
Assert.Empty (removed);
}

static bool ShouldIgnoreAssemblyAdd (string name) =>
// System.Configuration may cause these to load in the main AppDomain
name.StartsWith ("System.Configuration,", StringComparison.Ordinal) ||
name.StartsWith ("System.Xml.Linq,", StringComparison.Ordinal) ||
name.StartsWith ("System.Data,", StringComparison.Ordinal) ||
name.StartsWith ("System.Reflection,", StringComparison.Ordinal);

[Fact]
public async Task BadAppDomain ()
{
var testDir = TestDataPath.Get (nameof (LoadOpenApiDll));
var badGen = CreateGeneratorWithAppDomain (testDir, testDir);

badGen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_1_2_3.Combine ("lib", "netstandard2.0"));

var templateText = await testDir["LoadOpenApiDll.tt"].ReadAllTextNormalizedAsync ();

await Assert.ThrowsAnyAsync<TemplatingEngineException> (() => badGen.ProcessTemplateAsync ("LoadOpenApiDll.tt", templateText, null));
CleanupGenerator (badGen);
}

[Fact]
public async Task RunsInAppDomain ()
{
var gen = CreateGeneratorWithAppDomain ();
var templateText = "<#= System.AppDomain.CurrentDomain.FriendlyName #>";
var expectedOutputText = GetAppDomainNameForCurrentTest ();

var state = GetInitialState ();

var result = await gen.ProcessTemplateAsync ("TemplateInAppDomain.tt", templateText, null);

Assert.Null (gen.Errors.OfType<CompilerError> ().FirstOrDefault ());
Assert.Equal (expectedOutputText, result.content);

CleanupGenerator (gen);
VerifyFinalState (state);
}

static TestTemplateGeneratorWithAppDomain CreateGeneratorWithAppDomain (
string basePath = null, string relativeSearchPath = null, bool shadowCopy = false,
[CallerMemberName] string testName = null
)
{
if (basePath is null) {
// need to be able to resolve Mono.TextTemplating to load CompiledTemplate, which will resolve other assemblies via the host.
basePath = AppDomain.CurrentDomain.BaseDirectory;
if (relativeSearchPath is not null) {
throw new ArgumentException ($"{nameof (relativeSearchPath)} must be null if {nameof (basePath)} is null", nameof (relativeSearchPath));
}
relativeSearchPath = AppDomain.CurrentDomain.RelativeSearchPath;
}
return new (AppDomain.CreateDomain (
GetAppDomainNameForCurrentTest (testName),
null,
basePath,
relativeSearchPath,
shadowCopy)
);
}

static string GetAppDomainNameForCurrentTest ([CallerMemberName] string testName = null) => $"Template Test - {testName ?? "(unknown)"}";

class TestTemplateGeneratorWithAppDomain : TemplateGenerator
{
AppDomain appDomain;
public TestTemplateGeneratorWithAppDomain (AppDomain appDomain) => this.appDomain = appDomain;

public override AppDomain ProvideTemplatingAppDomain (string content) => appDomain;

public WeakReference ReleaseDomain ()
{
AppDomain.Unload (appDomain);
var weakRef = new WeakReference (appDomain);
appDomain = null;
return weakRef;
}
}
}

#endif
36 changes: 36 additions & 0 deletions Mono.TextTemplating.Tests/AssemblyLoadContextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NETCOREAPP3_0_OR_GREATER

using System;
using System.CodeDom.Compiler;
using System.Linq;
using System.Runtime.Loader;
using System.Threading.Tasks;
using Xunit;

namespace Mono.TextTemplating.Tests;

public class AssemblyLoadContextTests : AssemblyLoadTests<(SnapshotSet<string> assembliesInDefaultContext, SnapshotSet<AssemblyLoadContext> allContexts)>
{
protected override (SnapshotSet<string> assembliesInDefaultContext, SnapshotSet<AssemblyLoadContext> allContexts) GetInitialState () => (
Snapshot.LoadedAssemblies (),
Snapshot.AssemblyLoadContexts ()
);

protected override void VerifyFinalState ((SnapshotSet<string> assembliesInDefaultContext, SnapshotSet<AssemblyLoadContext> allContexts) state)
{
state.assembliesInDefaultContext.AssertUnchanged ();

// ensure unloadable contexts are collected
for (int i = 0; i < 10; i++) {
GC.Collect ();
GC.WaitForPendingFinalizers ();
}

state.allContexts.AssertUnchanged ();
}
}

#endif
95 changes: 95 additions & 0 deletions Mono.TextTemplating.Tests/AssemblyLoadTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.CodeDom.Compiler;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Xunit;

namespace Mono.TextTemplating.Tests;

public abstract class AssemblyLoadTests<T> : StatefulTest<T>
{
protected virtual TemplateGenerator CreateGenerator ([CallerMemberName] string testName = null) => new ();
protected virtual void CleanupGenerator (TemplateGenerator generator) { }

[Fact]
public async Task LoadOpenApiDll ()
{
var testDir = TestDataPath.Get ();
var gen = CreateGenerator ();
gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ());

var templatePath = testDir["LoadOpenApiDll.tt"];
var templateText = await templatePath.ReadAllTextNormalizedAsync ();

var expectedOutputPath = testDir["LoadOpenApiDll.yaml"];
var expectedOutputText = await expectedOutputPath.ReadAllTextNormalizedAsync ();

var state = GetInitialState ();

var result = await gen.ProcessTemplateAsync (templatePath, templateText, null);

Assert.Null (gen.Errors.OfType<CompilerError> ().FirstOrDefault ());
Assert.Equal (expectedOutputText, result.content);
Assert.Equal (expectedOutputPath, result.fileName);

CleanupGenerator (gen);
VerifyFinalState (state);
}

[Fact]
public async Task LoadOpenApiReadersDll ()
{
var testDir = TestDataPath.Get (nameof (LoadOpenApiDll));
var gen = CreateGenerator ();
gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ());
gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_Readers_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ());
gen.ReferencePaths.Add (PackagePath.SharpYaml_1_6_5.Combine ("lib", "netstandard2.0").AssertDirectoryExists ());

var templatePath = testDir["LoadOpenApiReaders.tt"];
var templateText = await templatePath.ReadAllTextNormalizedAsync ();

var state = GetInitialState ();

var result = await gen.ProcessTemplateAsync (templatePath, templateText, null);

Assert.Null (gen.Errors.OfType<CompilerError> ().FirstOrDefault ());
Assert.Equal ("Example", result.content);

CleanupGenerator (gen);
VerifyFinalState (state);
}

[FactExceptOnMono ("Mono incorrectly resolves the assembly if it has been loaded in a different AppDomain")]
public async Task MissingTransitiveReference ()
{
var gen = CreateGenerator ();
gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ());
gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_Readers_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ());

var testDir = TestDataPath.Get (nameof (LoadOpenApiDll));
var templatePath = testDir["LoadOpenApiReaders.tt"];
var templateText = await templatePath.ReadAllTextNormalizedAsync ();

await gen.ProcessTemplateAsync (templatePath, templateText, null);

var firstError = gen.Errors.OfType<CompilerError> ().FirstOrDefault ()?.ErrorText;
Assert.Contains ("FileNotFoundException: Could not load file or assembly 'SharpYaml, Version=1.6.5.0", firstError);

CleanupGenerator (gen);
}
}

public class FactExceptOnMonoAttribute : FactAttribute
{
public FactExceptOnMonoAttribute (string reason)
{
if (IsRunningOnMono) {
Skip = reason;
}
}

public static bool IsRunningOnMono { get; } = System.Type.GetType ("Mono.Runtime") != null;
}
7 changes: 7 additions & 0 deletions Mono.TextTemplating.Tests/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible",
Justification = "Many nested classes are test specific but need to be accessible from template code")]
85 changes: 85 additions & 0 deletions Mono.TextTemplating.Tests/LoadedAssembliesSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Xunit;

#if NETCOREAPP3_0_OR_GREATER
using System.Runtime.Loader;
#endif

namespace Mono.TextTemplating.Tests;

public abstract class Snapshot
{
public abstract void AssertUnchanged ();

#if FEATURE_APPDOMAINS
public static SnapshotSet<string> LoadedAssemblies (AppDomain context = null) => new (() => GetNames ((context ?? AppDomain.CurrentDomain).GetAssemblies ()));
#elif NETCOREAPP3_0_OR_GREATER
public static SnapshotSet<string> LoadedAssemblies (AssemblyLoadContext context = null) => new (() => GetNames ((context ?? AssemblyLoadContext.Default).Assemblies));
public static SnapshotSet<AssemblyLoadContext> AssemblyLoadContexts () => new (() => AssemblyLoadContext.All);
#endif

static IEnumerable<string> GetNames (IEnumerable<Assembly> assemblies)
{
var names = assemblies.Select (a => a.FullName);
if (!System.Diagnostics.Debugger.IsAttached) {
return names;
}
return names.Where (a => !a.StartsWith ("Microsoft.VisualStudio.Debugger", StringComparison.Ordinal));
}
}
public class SnapshotSet<TItem> : Snapshot
{
readonly Func<IEnumerable<TItem>> getCurrent;
readonly HashSet<TItem> initial;

public SnapshotSet (Func<IEnumerable<TItem>> getCurrent)
{
this.getCurrent = getCurrent;
initial = getCurrent ().ToHashSet ();
}

public override void AssertUnchanged ()
{
(var added, var removed) = GetChanges ();
Assert.Empty (added);
Assert.Empty (removed);
}

public (IEnumerable<TItem> added, IEnumerable<TItem> removed) GetChanges ()
{
var current = getCurrent ().ToHashSet ();
return (
current.Except (initial),
initial.Except (current)
);
}
}

public class AggregateSnapshot : Snapshot
{
readonly Snapshot[] snapshots;
public AggregateSnapshot (params Snapshot[] snapshots) => this.snapshots = snapshots;
public override void AssertUnchanged ()
{
foreach (var snapshot in snapshots) {
snapshot.AssertUnchanged ();
}
}
}

// these test process state so cannot be run in parallel with other tests
[CollectionDefinition (nameof (StatefulTests), DisableParallelization = true)]
public class StatefulTests { }

[Collection (nameof (StatefulTests))]
public abstract class StatefulTest<T>
{
protected abstract T GetInitialState ();
protected abstract void VerifyFinalState (T state);
}
Loading