Skip to content

Commit

Permalink
Merge pull request #1620 from nunit/issue-955
Browse files Browse the repository at this point in the history
Modify call sequence to agents so all arguments are named
  • Loading branch information
CharliePoole authored Feb 9, 2025
2 parents ac375dd + 9f2a65c commit 0414ced
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 16 deletions.
3 changes: 3 additions & 0 deletions NUnitConsole.sln
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NUnit3.10", "src\TestData\NUnit3.10\NUnit3.10.csproj", "{0555B97D-E918-455B-951C-74EFCDA8790A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NUnitCommon", "NUnitCommon", "{3B30D2E5-1587-4D68-B848-1BDDB3C24BFC}"
ProjectSection(SolutionItems) = preProject
src\NUnitCommon\Directory.Build.props = src\NUnitCommon\Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "nunit.extensibility.api", "src\NUnitCommon\nunit.extensibility.api\nunit.extensibility.api.csproj", "{71DE0F2C-C72B-4CBF-99BE-F2DC0FBEDA24}"
EndProject
Expand Down
78 changes: 78 additions & 0 deletions src/NUnitCommon/nunit.agent.core.tests/AgentOptionTests.cs
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));
}
}
}
124 changes: 124 additions & 0 deletions src/NUnitCommon/nunit.agent.core/AgentOptions.cs
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);
}
}
}
134 changes: 134 additions & 0 deletions src/NUnitCommon/nunit.agent.core/NUnitAgent.cs
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);
}
}
}
}
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
<Reference Include="System.Runtime.Remoting" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Program.cs" Link="Program.cs" />
</ItemGroup>

<ItemGroup>
<Content Include="..\..\..\..\nunit.ico">
<Link>nunit.ico</Link>
Expand All @@ -47,9 +43,9 @@
</ItemGroup>

<Copy SourceFiles="@(AgentFiles)" DestinationFiles="$(ConsoleDestination)%(FileName)%(Extension)" />
<Message Text="Copied @(AgentFiles->Count()) files to $(ConsoleDestination)" Importance="High" />
<Message Text="Copied @(AgentFiles-&gt;Count()) files to $(ConsoleDestination)" Importance="High" />
<Copy SourceFiles="@(AgentFiles)" DestinationFiles="$(EngineDestination)%(FileName)%(Extension)" />
<Message Text="Copied @(AgentFiles->Count()) files to $(EngineDestination)" Importance="High" />
<Message Text="Copied @(AgentFiles-&gt;Count()) files to $(EngineDestination)" Importance="High" />
</Target>

</Project>
9 changes: 9 additions & 0 deletions src/NUnitEngine/agents/nunit-agent-net462/Net462Agent.cs
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);
}
}
Loading

0 comments on commit 0414ced

Please sign in to comment.