diff --git a/src/NUnitEngine/mock-assembly/AccessesCurrentTestContextDuringDiscovery.cs b/src/NUnitEngine/mock-assembly/AccessesCurrentTestContextDuringDiscovery.cs
new file mode 100644
index 000000000..eb4897689
--- /dev/null
+++ b/src/NUnitEngine/mock-assembly/AccessesCurrentTestContextDuringDiscovery.cs
@@ -0,0 +1,19 @@
+using NUnit.Framework;
+
+namespace NUnit.Tests
+{
+ public class AccessesCurrentTestContextDuringDiscovery
+ {
+ public static int[] TestCases()
+ {
+ var _ = TestContext.CurrentContext;
+ return new[] { 0 };
+ }
+
+ [TestCaseSource(nameof(TestCases))]
+ public void Access_by_TestCaseSource(int arg) { }
+
+ [Test]
+ public void Access_by_ValueSource([ValueSource(nameof(TestCases))] int arg) { }
+ }
+}
diff --git a/src/NUnitEngine/mock-assembly/mock-assembly.csproj b/src/NUnitEngine/mock-assembly/mock-assembly.csproj
index d1291b594..d87b34cf5 100644
--- a/src/NUnitEngine/mock-assembly/mock-assembly.csproj
+++ b/src/NUnitEngine/mock-assembly/mock-assembly.csproj
@@ -87,6 +87,7 @@
Code
+
diff --git a/src/NUnitEngine/nunit.engine.tests/Helpers/On.cs b/src/NUnitEngine/nunit.engine.tests/Helpers/On.cs
new file mode 100644
index 000000000..7475cc8be
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Helpers/On.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Threading;
+
+namespace NUnit.Engine.Tests.Helpers
+{
+ public static class On
+ {
+ public static IDisposable Dispose(Action action) => new OnDisposeAction(action);
+ private sealed class OnDisposeAction : IDisposable
+ {
+ private Action action;
+
+ public OnDisposeAction(Action action)
+ {
+ this.action = action;
+ }
+
+ public void Dispose() => Interlocked.Exchange(ref action, null)?.Invoke();
+ }
+ }
+}
diff --git a/src/NUnitEngine/nunit.engine.tests/Helpers/ProcessUtils.cs b/src/NUnitEngine/nunit.engine.tests/Helpers/ProcessUtils.cs
new file mode 100644
index 000000000..9fd45b34d
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Helpers/ProcessUtils.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace NUnit.Engine.Tests.Helpers
+{
+ public static class ProcessUtils
+ {
+ public static ProcessResult Run(ProcessStartInfo startInfo)
+ {
+ startInfo.UseShellExecute = false;
+ startInfo.RedirectStandardOutput = true;
+ startInfo.RedirectStandardError = true;
+ startInfo.CreateNoWindow = true;
+
+ using (var process = new Process { StartInfo = startInfo })
+ {
+ var standardStreamData = new List();
+ var currentData = new StringBuilder();
+ var currentDataIsError = false;
+
+ process.OutputDataReceived += (sender, e) =>
+ {
+ if (e.Data == null) return;
+ if (currentDataIsError)
+ {
+ if (currentData.Length != 0)
+ standardStreamData.Add(new StandardStreamData(currentDataIsError, currentData.ToString()));
+ currentData = new StringBuilder();
+ currentDataIsError = false;
+ }
+ currentData.AppendLine(e.Data);
+ };
+ process.ErrorDataReceived += (sender, e) =>
+ {
+ if (e.Data == null) return;
+ if (!currentDataIsError)
+ {
+ if (currentData.Length != 0)
+ standardStreamData.Add(new StandardStreamData(currentDataIsError, currentData.ToString()));
+ currentData = new StringBuilder();
+ currentDataIsError = true;
+ }
+ currentData.AppendLine(e.Data);
+ };
+
+ process.Start();
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ process.WaitForExit();
+
+ if (currentData.Length != 0)
+ standardStreamData.Add(new StandardStreamData(currentDataIsError, currentData.ToString()));
+
+ return new ProcessResult(process.ExitCode, standardStreamData.ToArray());
+ }
+ }
+
+ [DebuggerDisplay("{ToString(),nq}")]
+ public struct ProcessResult
+ {
+ public ProcessResult(int exitCode, StandardStreamData[] standardStreamData)
+ {
+ ExitCode = exitCode;
+ StandardStreamData = standardStreamData;
+ }
+
+ public int ExitCode { get; }
+ public StandardStreamData[] StandardStreamData { get; }
+
+ public override string ToString() => ToString(true);
+
+ /// If true, appends "[stdout] " or "[stderr] " to the beginning of each line.
+ public string ToString(bool showStreamSource)
+ {
+ var r = new StringBuilder("Exit code ").Append(ExitCode);
+
+ if (StandardStreamData.Length != 0) r.AppendLine();
+
+ foreach (var data in StandardStreamData)
+ {
+ if (showStreamSource)
+ {
+ var lines = data.Data.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
+
+ // StandardStreamData.Data always ends with a blank line, so skip that
+ for (var i = 0; i < lines.Length - 1; i++)
+ r.Append(data.IsError ? "[stderr] " : "[stdout] ").AppendLine(lines[i]);
+ }
+ else
+ {
+ r.Append(data.Data);
+ }
+ }
+
+ return r.ToString();
+ }
+ }
+
+ [DebuggerDisplay("{ToString(),nq}")]
+ public struct StandardStreamData
+ {
+ public StandardStreamData(bool isError, string data)
+ {
+ IsError = isError;
+ Data = data;
+ }
+
+ public bool IsError { get; }
+ public string Data { get; }
+
+ public override string ToString() => (IsError ? "[stderr] " : "[stdout] ") + Data;
+ }
+ }
+}
diff --git a/src/NUnitEngine/nunit.engine.tests/Helpers/ShadowCopyUtils.cs b/src/NUnitEngine/nunit.engine.tests/Helpers/ShadowCopyUtils.cs
new file mode 100644
index 000000000..097d4a70f
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Helpers/ShadowCopyUtils.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace NUnit.Engine.Tests.Helpers
+{
+ public static class ShadowCopyUtils
+ {
+ ///
+ /// Returns the transitive closure of assemblies needed to copy.
+ /// Deals with assembly names rather than paths to work with runners that shadow copy.
+ ///
+ public static ICollection GetAllNeededAssemblyPaths(params string[] assemblyNames)
+ {
+ var r = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ var dependencies = StackEnumerator.Create(
+ from assemblyName in assemblyNames
+ select new AssemblyName(assemblyName));
+
+ foreach (var dependencyName in dependencies)
+ {
+ var dependency = Assembly.ReflectionOnlyLoad(dependencyName.FullName);
+
+ if (!dependency.GlobalAssemblyCache && r.Add(Path.GetFullPath(dependency.Location)))
+ {
+ dependencies.Recurse(dependency.GetReferencedAssemblies());
+ }
+ }
+
+ return r;
+ }
+ }
+}
diff --git a/src/NUnitEngine/nunit.engine.tests/Helpers/StackEnumerator.cs b/src/NUnitEngine/nunit.engine.tests/Helpers/StackEnumerator.cs
new file mode 100644
index 000000000..9aaaeb07b
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Helpers/StackEnumerator.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+namespace NUnit.Engine.Tests.Helpers
+{
+ public static class StackEnumerator
+ {
+ public static StackEnumerator Create(params T[] initial) => new StackEnumerator(initial);
+ public static StackEnumerator Create(IEnumerable initial) => new StackEnumerator(initial);
+ public static StackEnumerator Create(IEnumerator initial) => new StackEnumerator(initial);
+ }
+
+ public sealed class StackEnumerator : IDisposable
+ {
+ private readonly Stack> stack = new Stack>();
+ private IEnumerator current;
+
+ public bool MoveNext()
+ {
+ while (!current.MoveNext())
+ {
+ current.Dispose();
+ if (stack.Count == 0) return false;
+ current = stack.Pop();
+ }
+
+ return true;
+ }
+
+ public T Current => current.Current;
+
+ public void Recurse(IEnumerator newCurrent)
+ {
+ if (newCurrent == null) return;
+ stack.Push(current);
+ current = newCurrent;
+ }
+ public void Recurse(IEnumerable newCurrent)
+ {
+ if (newCurrent == null) return;
+ Recurse(newCurrent.GetEnumerator());
+ }
+ public void Recurse(params T[] newCurrent)
+ {
+ Recurse((IEnumerable)newCurrent);
+ }
+
+ public StackEnumerator(IEnumerator initial)
+ {
+ current = initial ?? Enumerable.Empty().GetEnumerator();
+ }
+ public StackEnumerator(IEnumerable initial) : this(initial?.GetEnumerator())
+ {
+ }
+ public StackEnumerator(params T[] initial) : this((IEnumerable)initial)
+ {
+ }
+
+ // Foreach support
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public StackEnumerator GetEnumerator()
+ {
+ return this;
+ }
+
+ public void Dispose()
+ {
+ current.Dispose();
+ foreach (var item in stack)
+ item.Dispose();
+ stack.Clear();
+ }
+ }
+}
diff --git a/src/NUnitEngine/nunit.engine.tests/Integration/DirectoryWithNeededAssemblies.cs b/src/NUnitEngine/nunit.engine.tests/Integration/DirectoryWithNeededAssemblies.cs
new file mode 100644
index 000000000..7cbfa6cd9
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Integration/DirectoryWithNeededAssemblies.cs
@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using NUnit.Engine.Tests.Helpers;
+
+namespace NUnit.Engine.Tests.Integration
+{
+ internal sealed class DirectoryWithNeededAssemblies : IDisposable
+ {
+ public string Directory { get; }
+
+ ///
+ /// Returns the transitive closure of assemblies needed to copy.
+ /// Deals with assembly names rather than paths to work with runners that shadow copy.
+ ///
+ public DirectoryWithNeededAssemblies(params string[] assemblyNames)
+ {
+ Directory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ System.IO.Directory.CreateDirectory(Directory);
+
+ foreach (var neededAssembly in ShadowCopyUtils.GetAllNeededAssemblyPaths(assemblyNames))
+ {
+ File.Copy(neededAssembly, Path.Combine(Directory, Path.GetFileName(neededAssembly)));
+ }
+ }
+
+ public void Dispose()
+ {
+ System.IO.Directory.Delete(Directory, true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NUnitEngine/nunit.engine.tests/Integration/IntegrationTests.cs b/src/NUnitEngine/nunit.engine.tests/Integration/IntegrationTests.cs
new file mode 100644
index 000000000..8e51df28a
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Integration/IntegrationTests.cs
@@ -0,0 +1,9 @@
+using NUnit.Framework;
+
+namespace NUnit.Engine.Tests.Integration
+{
+ [TestFixture, Category("Integration")]
+ public abstract class IntegrationTests
+ {
+ }
+}
diff --git a/src/NUnitEngine/nunit.engine.tests/Integration/MockAssemblyInDirectoryWithFramework.cs b/src/NUnitEngine/nunit.engine.tests/Integration/MockAssemblyInDirectoryWithFramework.cs
new file mode 100644
index 000000000..068dba825
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Integration/MockAssemblyInDirectoryWithFramework.cs
@@ -0,0 +1,25 @@
+using System;
+using System.IO;
+using NUnit.Framework;
+
+namespace NUnit.Engine.Tests.Integration
+{
+ internal sealed class MockAssemblyInDirectoryWithFramework : IDisposable
+ {
+ private readonly DirectoryWithNeededAssemblies directory;
+
+ public string MockAssemblyDll => Path.Combine(directory.Directory, "mock-assembly.dll");
+
+ public MockAssemblyInDirectoryWithFramework()
+ {
+ directory = new DirectoryWithNeededAssemblies("mock-assembly");
+
+ Assert.That(Path.Combine(directory.Directory, "nunit.framework.dll"), Does.Exist, "This test must be run with nunit.framework.dll in the same directory as the mock assembly.");
+ }
+
+ public void Dispose()
+ {
+ directory.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NUnitEngine/nunit.engine.tests/Integration/RemoteAgentTests.cs b/src/NUnitEngine/nunit.engine.tests/Integration/RemoteAgentTests.cs
new file mode 100644
index 000000000..a1830bd2d
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Integration/RemoteAgentTests.cs
@@ -0,0 +1,25 @@
+using System.Diagnostics;
+using NUnit.Engine.Tests.Helpers;
+using NUnit.Framework;
+
+namespace NUnit.Engine.Tests.Integration
+{
+ public sealed class RemoteAgentTests : IntegrationTests
+ {
+ [Test]
+ public void Explore_does_not_throw_SocketException()
+ {
+ using (var runner = new RunnerInDirectoryWithoutFramework())
+ using (var test = new MockAssemblyInDirectoryWithFramework())
+ {
+ for (var times = 0; times < 3; times++)
+ {
+ var result = ProcessUtils.Run(new ProcessStartInfo(runner.ConsoleExe, $"--explore \"{test.MockAssemblyDll}\""));
+ Assert.That(result.StandardStreamData, Has.None.With.Property("Data").Contains("System.Net.Sockets.SocketException"));
+ Assert.That(result.StandardStreamData, Has.None.With.Property("IsError").True);
+ Assert.That(result, Has.Property("ExitCode").Zero);
+ }
+ }
+ }
+ }
+}
diff --git a/src/NUnitEngine/nunit.engine.tests/Integration/RunnerInDirectoryWithoutFramework.cs b/src/NUnitEngine/nunit.engine.tests/Integration/RunnerInDirectoryWithoutFramework.cs
new file mode 100644
index 000000000..2db48a21f
--- /dev/null
+++ b/src/NUnitEngine/nunit.engine.tests/Integration/RunnerInDirectoryWithoutFramework.cs
@@ -0,0 +1,25 @@
+using System;
+using System.IO;
+using NUnit.Framework;
+
+namespace NUnit.Engine.Tests.Integration
+{
+ internal sealed class RunnerInDirectoryWithoutFramework : IDisposable
+ {
+ private readonly DirectoryWithNeededAssemblies directory;
+
+ public string ConsoleExe => Path.Combine(directory.Directory, "nunit3-console.exe");
+
+ public RunnerInDirectoryWithoutFramework()
+ {
+ directory = new DirectoryWithNeededAssemblies("nunit3-console", "nunit-agent", "nunit-agent-x86");
+
+ Assert.That(Path.Combine(directory.Directory, "nunit.framework.dll"), Does.Not.Exist, "This test must be run without nunit.framework.dll in the same directory as the console runner.");
+ }
+
+ public void Dispose()
+ {
+ directory.Dispose();
+ }
+ }
+}
diff --git a/src/NUnitEngine/nunit.engine.tests/nunit.engine.tests.csproj b/src/NUnitEngine/nunit.engine.tests/nunit.engine.tests.csproj
index 98fdede13..415eb89ce 100644
--- a/src/NUnitEngine/nunit.engine.tests/nunit.engine.tests.csproj
+++ b/src/NUnitEngine/nunit.engine.tests/nunit.engine.tests.csproj
@@ -79,6 +79,15 @@
+
+
+
+
+
+
+
+
+