Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 967286d

Browse files
committed
Centralize RemoteExecutor test support into Common
For the interprocess communication tests, I'd written a small console app and launcher that makes it easier for a process to remote some logic to another process. This commit generalizes that logic and moves it into Common, with the goal of making it available for other test suites to use. In the process of doing so, I added some functionality, like the ability to use lambdas (without closures).
1 parent 652d9eb commit 967286d

File tree

16 files changed

+315
-175
lines changed

16 files changed

+315
-175
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Reflection;
6+
7+
[assembly: System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
8+
9+
namespace RemoteExecutorConsoleApp
10+
{
11+
/// <summary>
12+
/// Provides an entry point in a new process that will load a specified method and invoke it.
13+
/// </summary>
14+
static class Program
15+
{
16+
static int Main(string[] args)
17+
{
18+
// The program expects to be passed the target assembly name to load, the type
19+
// from that assembly to find, and the method from that assembly to invoke.
20+
// Any additional arguments are passed as strings to the method.
21+
if (args.Length < 3)
22+
{
23+
Console.Error.WriteLine("Usage: {0} assemblyName typeName methodName", typeof(Program).GetTypeInfo().Assembly.GetName().Name);
24+
return -1;
25+
}
26+
string assemblyName = args[0];
27+
string typeName = args[1];
28+
string methodName = args[2];
29+
string[] additionalArgs = args.Length > 3 ?
30+
args.Subarray(3, args.Length - 3) :
31+
Array.Empty<string>();
32+
33+
// Load the specified assembly, type, and method, then invoke the method.
34+
// The program's exit code is the return value of the invoked method.
35+
Assembly a = null;
36+
Type t = null;
37+
MethodInfo mi = null;
38+
object instance = null;
39+
try
40+
{
41+
a = Assembly.Load(new AssemblyName(assemblyName));
42+
t = a.GetType(typeName);
43+
mi = t.GetMethod(methodName);
44+
if (!mi.IsStatic)
45+
{
46+
instance = Activator.CreateInstance(t);
47+
}
48+
return (int)mi.Invoke(instance, additionalArgs);
49+
}
50+
catch (Exception exc)
51+
{
52+
Console.Error.WriteLine("Exception from RemoteExecutorConsoleApp({0}):", string.Join(", ", args));
53+
Console.Error.WriteLine("Assembly: {0}", a);
54+
Console.Error.WriteLine("Type: {0}", t);
55+
Console.Error.WriteLine("Method: {0}", mi);
56+
Console.Error.WriteLine("Exception: {0}", exc);
57+
return -2;
58+
}
59+
finally
60+
{
61+
IDisposable d = instance as IDisposable;
62+
if (d != null)
63+
{
64+
d.Dispose();
65+
}
66+
}
67+
}
68+
69+
private static MethodInfo GetMethod(this Type type, string methodName)
70+
{
71+
Type t = type;
72+
while (t != null)
73+
{
74+
TypeInfo ti = t.GetTypeInfo();
75+
MethodInfo mi = ti.GetDeclaredMethod(methodName);
76+
if (mi != null)
77+
{
78+
return mi;
79+
}
80+
t = ti.BaseType;
81+
}
82+
return null;
83+
}
84+
85+
private static T[] Subarray<T>(this T[] arr, int offset, int count)
86+
{
87+
var newArr = new T[count];
88+
Array.Copy(arr, offset, newArr, 0, count);
89+
return newArr;
90+
}
91+
}
92+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<PropertyGroup>
55
<ProjectGuid>{F5E941C8-AF2F-47AB-A066-FF25470CE382}</ProjectGuid>
66
<OutputType>Exe</OutputType>
7-
<RootNamespace>InterProcessCommunication.Tests</RootNamespace>
8-
<AssemblyName>InterProcessCommunication.TestConsoleApp</AssemblyName>
7+
<RootNamespace>RemoteExecutorConsoleApp</RootNamespace>
8+
<AssemblyName>RemoteExecutorConsoleApp</AssemblyName>
99
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
1010
</PropertyGroup>
1111
<!-- Default configurations to help VS understand the configurations -->
1212
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' " />
1313
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' " />
1414
<ItemGroup>
15-
<Compile Include="TestConsoleApp.cs" />
15+
<Compile Include="RemoteExecutorConsoleApp.cs" />
1616
<Compile Include="$(CommonPath)\System\Diagnostics\CodeAnalysis\ExcludeFromCodeCoverageAttribute.cs">
1717
<Link>Common\System\Diagnostics\CodeAnalysis\ExcludeFromCodeCoverageAttribute.cs</Link>
1818
</Compile>
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.IO;
5+
using System.Reflection;
6+
using Xunit;
7+
8+
namespace System.Diagnostics
9+
{
10+
/// <summary>Base class used for all tests that need to spawn a remote process.</summary>
11+
public abstract class RemoteExecutorTestBase : FileCleanupTestBase
12+
{
13+
/// <summary>The CoreCLR host used to host the test console app.</summary>
14+
protected const string HostRunner = "corerun";
15+
/// <summary>The name of the test console app.</summary>
16+
protected const string TestConsoleApp = "RemoteExecutorConsoleApp.exe";
17+
18+
/// <summary>A timeout (milliseconds) after which a wait on a remote operation should be considered a failure.</summary>
19+
internal const int FailWaitTimeoutMilliseconds = 30 * 1000;
20+
/// <summary>The exit code returned when the test process exits successfully.</summary>
21+
internal const int SuccessExitCode = 42;
22+
23+
/// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary>
24+
/// <param name="method">The method to invoke.</param>
25+
/// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param>
26+
internal static RemoteInvokeHandle RemoteInvoke(
27+
Func<int> method,
28+
bool start = true)
29+
{
30+
return RemoteInvoke(method.GetMethodInfo(), Array.Empty<string>(), start);
31+
}
32+
33+
/// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary>
34+
/// <param name="method">The method to invoke.</param>
35+
/// <param name="arg1">The first argument to pass to the method.</param>
36+
/// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param>
37+
internal static RemoteInvokeHandle RemoteInvoke(
38+
Func<string, int> method,
39+
string arg,
40+
bool start = true)
41+
{
42+
return RemoteInvoke(method.GetMethodInfo(), new[] { arg }, start);
43+
}
44+
45+
/// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary>
46+
/// <param name="method">The method to invoke.</param>
47+
/// <param name="arg1">The first argument to pass to the method.</param>
48+
/// <param name="arg2">The second argument to pass to the method.</param>
49+
/// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param>
50+
internal static RemoteInvokeHandle RemoteInvoke(
51+
Func<string, string, int> method,
52+
string arg1, string arg2,
53+
bool start = true)
54+
{
55+
return RemoteInvoke(method.GetMethodInfo(), new[] { arg1, arg2 }, start);
56+
}
57+
58+
/// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary>
59+
/// <param name="method">The method to invoke.</param>
60+
/// <param name="arg1">The first argument to pass to the method.</param>
61+
/// <param name="arg2">The second argument to pass to the method.</param>
62+
/// <param name="arg3">The third argument to pass to the method.</param>
63+
/// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param>
64+
internal static RemoteInvokeHandle RemoteInvoke(
65+
Func<string, string, string, int> method,
66+
string arg1, string arg2, string arg3,
67+
bool start = true)
68+
{
69+
return RemoteInvoke(method.GetMethodInfo(), new[] { arg1, arg2, arg3 }, start);
70+
}
71+
72+
/// <summary>Invokes the method from this assembly in another process using the specified arguments.</summary>
73+
/// <param name="method">The method to invoke.</param>
74+
/// <param name="args">The arguments to pass to the method.</param>
75+
/// <param name="start">true if this function should Start the Process; false if that responsibility is left up to the caller.</param>
76+
private static RemoteInvokeHandle RemoteInvoke(MethodInfo method, string[] args, bool start)
77+
{
78+
// Verify the specified method is and that it returns an int (the exit code),
79+
// and that if it accepts any arguments, they're all strings.
80+
Assert.Equal(typeof(int), method.ReturnType);
81+
Assert.All(method.GetParameters(), pi => Assert.Equal(typeof(string), pi.ParameterType));
82+
83+
// And make sure it's in this assembly. This isn't critical, but it helps with deployment to know
84+
// that the method to invoke is available because we're already running in this assembly.
85+
Type t = method.DeclaringType;
86+
Assembly a = t.GetTypeInfo().Assembly;
87+
Assert.Equal(typeof(RemoteExecutorTestBase).GetTypeInfo().Assembly, a);
88+
89+
// Start the other process and return a wrapper for it to handle its lifetime and exit checking.
90+
ProcessStartInfo psi = new ProcessStartInfo()
91+
{
92+
FileName = HostRunner,
93+
Arguments = TestConsoleApp + " \"" + a.FullName + "\" " + t.FullName + " " + method.Name + " " + string.Join(" ", args),
94+
UseShellExecute = false,
95+
CreateNoWindow = false
96+
};
97+
98+
// Profilers / code coverage tools doing coverage of the test process set environment
99+
// variables to tell the targeted process what profiler to load. We don't want the child process
100+
// to be profiled / have code coverage, so we remove these environment variables for that process
101+
// before it's started.
102+
psi.Environment.Remove("Cor_Profiler");
103+
psi.Environment.Remove("Cor_Enable_Profiling");
104+
psi.Environment.Remove("CoreClr_Profiler");
105+
psi.Environment.Remove("CoreClr_Enable_Profiling");
106+
107+
// Return the handle to the process, which may or not be started
108+
return new RemoteInvokeHandle(start ?
109+
Process.Start(psi) :
110+
new Process() { StartInfo = psi });
111+
}
112+
113+
/// <summary>A cleanup handle to the Process created for the remote invocation.</summary>
114+
internal sealed class RemoteInvokeHandle : IDisposable
115+
{
116+
public RemoteInvokeHandle(Process process)
117+
{
118+
Process = process;
119+
}
120+
121+
public Process Process { get; private set; }
122+
123+
public void Dispose()
124+
{
125+
if (Process != null)
126+
{
127+
// A bit unorthodox to do throwing operations in a Dispose, but by doing it here we avoid
128+
// needing to do this in every derived test and keep each test much simpler.
129+
try
130+
{
131+
Assert.True(Process.WaitForExit(FailWaitTimeoutMilliseconds));
132+
Assert.Equal(SuccessExitCode, Process.ExitCode);
133+
}
134+
finally
135+
{
136+
// Cleanup
137+
try { Process.Kill(); }
138+
catch { } // ignore all cleanup errors
139+
140+
Process.Dispose();
141+
Process = null;
142+
}
143+
}
144+
}
145+
}
146+
}
147+
}

src/Scenarios/InterProcessCommunication.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ VisualStudioVersion = 14.0.22823.1
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterProcessCommunication.Tests", "tests\InterProcessCommunication\InterProcessCommunication.Tests.csproj", "{C8B651EA-21B8-45B3-9617-3BA952D0F12B}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterProcessCommunication.TestConsoleApp", "tests\InterProcessCommunication\InterProcessCommunication.TestConsoleApp\InterProcessCommunication.TestConsoleApp.csproj", "{F5E941C8-AF2F-47AB-A066-FF25470CE382}"
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteExecutorConsoleApp", "..\Common\tests\System\Diagnostics\RemoteExecutorConsoleApp\RemoteExecutorConsoleApp.csproj", "{F5E941C8-AF2F-47AB-A066-FF25470CE382}"
99
EndProject
1010
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{EFFF9E5B-58A0-4D0E-891D-40EF450FE7DA}"
1111
ProjectSection(SolutionItems) = preProject

src/Scenarios/tests/InterProcessCommunication/AnonymousPipesTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.Diagnostics;
45
using System.IO;
56
using System.IO.Pipes;
67
using Xunit;
78

89
namespace InterProcessCommunication.Tests
910
{
10-
public class AnonymousPipesTests : IpcTestBase
11+
public class AnonymousPipesTests : RemoteExecutorTestBase
1112
{
1213
[Fact]
1314
public void PingPong()
@@ -16,7 +17,7 @@ public void PingPong()
1617
// Then spawn another process to communicate with.
1718
using (var outbound = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
1819
using (var inbound = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable))
19-
using (var remote = RemoteInvoke("PingPong_OtherProcess", outbound.GetClientHandleAsString(), inbound.GetClientHandleAsString()))
20+
using (var remote = RemoteInvoke(PingPong_OtherProcess, outbound.GetClientHandleAsString(), inbound.GetClientHandleAsString()))
2021
{
2122
// Close our local copies of the handles now that we've passed them of to the other process
2223
outbound.DisposeLocalCopyOfClientHandle();
@@ -32,7 +33,7 @@ public void PingPong()
3233
}
3334
}
3435

35-
public static int PingPong_OtherProcess(string inHandle, string outHandle)
36+
private static int PingPong_OtherProcess(string inHandle, string outHandle)
3637
{
3738
// Create the clients associated with the supplied handles
3839
using (var inbound = new AnonymousPipeClientStream(PipeDirection.In, inHandle))

src/Scenarios/tests/InterProcessCommunication/EventWaitHandleTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Threading;
67
using Xunit;
78

89
namespace InterProcessCommunication.Tests
910
{
10-
public class EventWaitHandleTests : IpcTestBase
11+
public class EventWaitHandleTests : RemoteExecutorTestBase
1112
{
1213
[ActiveIssue("https://github.com/dotnet/coreclr/issues/1237", PlatformID.AnyUnix)]
1314
[Theory]
@@ -22,7 +23,7 @@ public void PingPong(EventResetMode mode)
2223
// Create the two events and the other process with which to synchronize
2324
using (var inbound = new EventWaitHandle(true, mode, inboundName))
2425
using (var outbound = new EventWaitHandle(false, mode, outboundName))
25-
using (var remote = RemoteInvoke("PingPong_OtherProcess", mode.ToString(), outboundName, inboundName))
26+
using (var remote = RemoteInvoke(PingPong_OtherProcess, mode.ToString(), outboundName, inboundName))
2627
{
2728
// Repeatedly wait for one event and then set the other
2829
for (int i = 0; i < 10; i++)
@@ -37,7 +38,7 @@ public void PingPong(EventResetMode mode)
3738
}
3839
}
3940

40-
public static int PingPong_OtherProcess(string modeName, string inboundName, string outboundName)
41+
private static int PingPong_OtherProcess(string modeName, string inboundName, string outboundName)
4142
{
4243
EventResetMode mode = (EventResetMode)Enum.Parse(typeof(EventResetMode), modeName);
4344

src/Scenarios/tests/InterProcessCommunication/InterProcessCommunication.TestConsoleApp/TestConsoleApp.cs

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)