-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1620 from nunit/issue-955
Modify call sequence to agents so all arguments are named
- Loading branch information
Showing
12 changed files
with
370 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
src/NUnitCommon/nunit.agent.core.tests/AgentOptionTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt | ||
|
||
using System; | ||
using NUnit.Engine; | ||
using NUnit.Framework; | ||
|
||
namespace NUnit.Agents | ||
{ | ||
public class AgentOptionTests | ||
{ | ||
static TestCaseData[] DefaultSettings = new[] | ||
{ | ||
new TestCaseData("AgentId", Guid.Empty), | ||
new TestCaseData("AgencyUrl", string.Empty), | ||
new TestCaseData("AgencyPid", string.Empty), | ||
new TestCaseData("DebugAgent", false), | ||
new TestCaseData("DebugTests", false), | ||
new TestCaseData("TraceLevel", InternalTraceLevel.Off), | ||
new TestCaseData("WorkDirectory", string.Empty) | ||
}; | ||
|
||
[TestCaseSource(nameof(DefaultSettings))] | ||
public void DefaultOptionSettings<T>(string propertyName, T defaultValue) | ||
{ | ||
var options = new AgentOptions(); | ||
var prop = typeof(AgentOptions).GetProperty(propertyName); | ||
Assert.That(prop, Is.Not.Null, $"Property {propertyName} does not exist"); | ||
Assert.That(prop.GetValue(options, new object[0]), Is.EqualTo(defaultValue)); | ||
} | ||
|
||
static readonly Guid AGENT_GUID = Guid.NewGuid(); | ||
static readonly TestCaseData[] ValidSettings = new[] | ||
{ | ||
// Boolean options - no values provided | ||
new TestCaseData("--debug-agent", "DebugAgent", true), | ||
new TestCaseData("--debug-tests", "DebugTests", true), | ||
// Options with values - using '=' as delimiter | ||
new TestCaseData($"--agentId={AGENT_GUID}", "AgentId", AGENT_GUID), | ||
new TestCaseData("--agencyUrl=THEURL", "AgencyUrl", "THEURL"), | ||
new TestCaseData("--pid=1234", "AgencyPid", "1234"), | ||
new TestCaseData("--trace=Info", "TraceLevel", InternalTraceLevel.Info), | ||
new TestCaseData("--work=WORKDIR", "WorkDirectory", "WORKDIR"), | ||
// Options with values - using ':' as delimiter | ||
new TestCaseData("--trace:Error", "TraceLevel", InternalTraceLevel.Error), | ||
new TestCaseData("--work:WORKDIR", "WorkDirectory", "WORKDIR"), | ||
// Value with spaces (provided OS passes them through) | ||
new TestCaseData("--work:MY WORK DIR", "WorkDirectory", "MY WORK DIR"), | ||
}; | ||
|
||
[TestCaseSource(nameof(ValidSettings))] | ||
public void ValidOptionSettings<T>(string option, string propertyName, T expectedValue) | ||
{ | ||
var options = new AgentOptions(option); | ||
var prop = typeof(AgentOptions).GetProperty(propertyName); | ||
Assert.That(prop, Is.Not.Null, $"Property {propertyName} does not exist"); | ||
Assert.That(prop.GetValue(options, new object[0]), Is.EqualTo(expectedValue)); | ||
} | ||
|
||
[Test] | ||
public void MultipleOptions() | ||
{ | ||
var options = new AgentOptions("--debug-tests", "--trace=Info", "--work", "MYWORKDIR"); | ||
Assert.That(options.DebugAgent, Is.False); | ||
Assert.That(options.DebugTests); | ||
Assert.That(options.TraceLevel, Is.EqualTo(InternalTraceLevel.Info)); | ||
Assert.That(options.WorkDirectory, Is.EqualTo("MYWORKDIR")); | ||
} | ||
|
||
[Test] | ||
public void FileNameSupplied() | ||
{ | ||
var filename = GetType().Assembly.Location; | ||
var options = new AgentOptions(filename); | ||
Assert.That(options.Files.Count, Is.EqualTo(1)); | ||
Assert.That(options.Files[0], Is.EqualTo(filename)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt | ||
|
||
using NUnit.Engine; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
|
||
namespace NUnit.Agents | ||
{ | ||
/// <summary> | ||
/// All agents, either built-in or pluggable, must be able to | ||
/// handle the options defined in this class. In some cases, | ||
/// it may be permissible to ignore them but they should never | ||
/// give rise to an error. | ||
/// </summary> | ||
public class AgentOptions | ||
{ | ||
static readonly char[] DELIMS = new[] { '=', ':' }; | ||
// Dictionary containing valid options with bool value true if a value is required. | ||
static readonly Dictionary<string, bool> VALID_OPTIONS = new Dictionary<string, bool>(); | ||
|
||
static AgentOptions() | ||
{ | ||
VALID_OPTIONS["agentId"] = true; | ||
VALID_OPTIONS["agencyUrl"] = true; | ||
VALID_OPTIONS["debug-agent"] = false; | ||
VALID_OPTIONS["debug-tests"] = false; | ||
VALID_OPTIONS["trace"] = true; | ||
VALID_OPTIONS["pid"] = true; | ||
VALID_OPTIONS["work"] = true; | ||
} | ||
|
||
public AgentOptions(params string[] args) | ||
{ | ||
int index; | ||
for (index = 0; index < args.Length; index++) | ||
{ | ||
string arg = args[index]; | ||
|
||
if (IsOption(arg)) | ||
{ | ||
var option = arg.Substring(2); | ||
var delim = option.IndexOfAny(DELIMS); | ||
var opt = option; | ||
string? val = null; | ||
if (delim > 0) | ||
{ | ||
opt = option.Substring(0, delim); | ||
val = option.Substring(delim + 1); | ||
} | ||
|
||
// Simultaneously check that the option is valid and determine if it takes an argument | ||
if (!VALID_OPTIONS.TryGetValue(opt, out bool optionTakesValue)) | ||
throw new Exception($"Invalid argument: {arg}"); | ||
|
||
if (optionTakesValue) | ||
{ | ||
if (val == null && index + 1 < args.Length) | ||
val = args[++index]; | ||
|
||
if (val == null) | ||
throw new Exception($"Option requires a value: {arg}"); | ||
} | ||
else if (delim > 0) | ||
{ | ||
throw new Exception($"Option does not take a value: {arg}"); | ||
} | ||
|
||
if (opt == "agentId") | ||
AgentId = new Guid(GetArgumentValue(arg)); | ||
else if (opt == "agencyUrl") | ||
AgencyUrl = GetArgumentValue(arg); | ||
else if (opt == "debug-agent") | ||
DebugAgent = true; | ||
else if (opt == "debug-tests") | ||
DebugTests = true; | ||
else if (opt == "trace") | ||
TraceLevel = (InternalTraceLevel)Enum.Parse(typeof(InternalTraceLevel), val.ShouldNotBeNull()); | ||
else if (opt == "pid") | ||
AgencyPid = val.ShouldNotBeNull(); | ||
else if (opt == "work") | ||
WorkDirectory = val.ShouldNotBeNull(); | ||
else | ||
throw new Exception($"Invalid argument: {arg}"); | ||
} | ||
else if (File.Exists(arg)) | ||
Files.Add(arg); | ||
else | ||
throw new FileNotFoundException($"FileNotFound: {arg}"); | ||
} | ||
|
||
if (Files.Count > 1) | ||
throw new ArgumentException($"Only one file argument is allowed but {Files.Count} were supplied"); | ||
|
||
string GetArgumentValue(string argument) | ||
{ | ||
var delim = argument.IndexOfAny(DELIMS); | ||
|
||
if (delim > 0) | ||
return argument.Substring(delim + 1); | ||
|
||
if (index + 1 < args.Length) | ||
return args[++index]; | ||
|
||
throw new Exception($"Option requires a value: {argument}"); | ||
} | ||
} | ||
|
||
public Guid AgentId { get; } = Guid.Empty; | ||
public string AgencyUrl { get; } = string.Empty; | ||
public string AgencyPid { get; } = string.Empty; | ||
public bool DebugTests { get; } = false; | ||
public bool DebugAgent { get; } = false; | ||
public InternalTraceLevel TraceLevel { get; } = InternalTraceLevel.Off; | ||
public string WorkDirectory { get; } = string.Empty; | ||
|
||
public List<string> Files { get; } = new List<string>(); | ||
|
||
private static bool IsOption(string arg) | ||
{ | ||
return arg.StartsWith("--", StringComparison.Ordinal); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt | ||
|
||
using System; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Security; | ||
using System.Reflection; | ||
using NUnit.Engine.Agents; | ||
|
||
#if NETFRAMEWORK | ||
using NUnit.Engine.Communication.Transports.Remoting; | ||
#else | ||
using NUnit.Engine.Communication.Transports.Tcp; | ||
#endif | ||
|
||
namespace NUnit.Agents | ||
{ | ||
public class NUnitAgent<TAgent> | ||
{ | ||
static Process? AgencyProcess; | ||
static RemoteTestAgent? Agent; | ||
static readonly int _pid = Process.GetCurrentProcess().Id; | ||
static readonly Logger log = InternalTrace.GetLogger(typeof(TestAgent)); | ||
|
||
/// <summary> | ||
/// The main entry point for the application. | ||
/// </summary> | ||
[STAThread] | ||
public static void Execute(string[] args) | ||
{ | ||
var options = new AgentOptions(args); | ||
var logName = $"nunit-agent_{_pid}.log"; | ||
|
||
InternalTrace.Initialize(Path.Combine(options.WorkDirectory, logName), options.TraceLevel); | ||
log.Info($"{typeof(TAgent).Name} process {_pid} starting"); | ||
log.Info($" Agent Path: {Assembly.GetExecutingAssembly().Location}"); | ||
|
||
if (options.DebugAgent || options.DebugTests) | ||
TryLaunchDebugger(); | ||
|
||
log.Info($" AgentId: {options.AgentId}"); | ||
log.Info($" AgencyUrl: {options.AgencyUrl}"); | ||
log.Info($" AgencyPid: {options.AgencyPid}"); | ||
|
||
if (!string.IsNullOrEmpty(options.AgencyPid)) | ||
LocateAgencyProcess(options.AgencyPid); | ||
|
||
log.Info("Starting RemoteTestAgent"); | ||
Agent = new RemoteTestAgent(options.AgentId); | ||
#if NETFRAMEWORK | ||
Agent.Transport = new TestAgentRemotingTransport(Agent, options.AgencyUrl); | ||
#else | ||
Agent.Transport = new TestAgentTcpTransport(Agent, options.AgencyUrl); | ||
#endif | ||
|
||
try | ||
{ | ||
if (Agent.Start()) | ||
WaitForStop(Agent, AgencyProcess.ShouldNotBeNull()); | ||
else | ||
{ | ||
log.Error("Failed to start RemoteTestAgent"); | ||
Environment.Exit(AgentExitCodes.FAILED_TO_START_REMOTE_AGENT); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
log.Error("Exception in RemoteTestAgent. {0}", ExceptionHelper.BuildMessageAndStackTrace(ex)); | ||
Environment.Exit(AgentExitCodes.UNEXPECTED_EXCEPTION); | ||
} | ||
log.Info("Agent process {0} exiting cleanly", _pid); | ||
|
||
Environment.Exit(AgentExitCodes.OK); | ||
} | ||
|
||
private static void LocateAgencyProcess(string agencyPid) | ||
{ | ||
var agencyProcessId = int.Parse(agencyPid); | ||
try | ||
{ | ||
AgencyProcess = Process.GetProcessById(agencyProcessId); | ||
} | ||
catch (Exception e) | ||
{ | ||
log.Error($"Unable to connect to agency process with PID: {agencyProcessId}"); | ||
log.Error($"Failed with exception: {e.Message} {e.StackTrace}"); | ||
Environment.Exit(AgentExitCodes.UNABLE_TO_LOCATE_AGENCY); | ||
} | ||
} | ||
|
||
private static void WaitForStop(RemoteTestAgent agent, Process agencyProcess) | ||
{ | ||
log.Debug("Waiting for stopSignal"); | ||
|
||
while (!agent.WaitForStop(500)) | ||
{ | ||
if (agencyProcess.HasExited) | ||
{ | ||
log.Error("Parent process has been terminated."); | ||
Environment.Exit(AgentExitCodes.PARENT_PROCESS_TERMINATED); | ||
} | ||
} | ||
|
||
log.Debug("Stop signal received"); | ||
} | ||
|
||
private static void TryLaunchDebugger() | ||
{ | ||
if (Debugger.IsAttached) | ||
return; | ||
|
||
try | ||
{ | ||
Debugger.Launch(); | ||
} | ||
catch (SecurityException se) | ||
{ | ||
if (InternalTrace.Initialized) | ||
{ | ||
log.Error($"System.Security.Permissions.UIPermission is not set to start the debugger. {se} {se.StackTrace}"); | ||
} | ||
Environment.Exit(AgentExitCodes.DEBUGGER_SECURITY_VIOLATION); | ||
} | ||
catch (NotImplementedException nie) //Debugger is not implemented on mono | ||
{ | ||
if (InternalTrace.Initialized) | ||
{ | ||
log.Error($"Debugger is not available on all platforms. {nie} {nie.StackTrace}"); | ||
} | ||
Environment.Exit(AgentExitCodes.DEBUGGER_NOT_IMPLEMENTED); | ||
} | ||
} | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
src/NUnitEngine/agents/nunit-agent-net462-x86/Net462X86Agent.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt | ||
|
||
namespace NUnit.Agents | ||
{ | ||
public class Net462X86Agent : NUnitAgent<Net462X86Agent> | ||
{ | ||
public static void Main(string[] args) => NUnitAgent<Net462X86Agent>.Execute(args); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt | ||
|
||
namespace NUnit.Agents | ||
{ | ||
public class Net462Agent : NUnitAgent<Net462Agent> | ||
{ | ||
public static void Main(string[] args) => NUnitAgent<Net462Agent>.Execute(args); | ||
} | ||
} |
Oops, something went wrong.