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 @@ + + + + + + + + +