Skip to content

Commit 0414ced

Browse files
authored
Merge pull request #1620 from nunit/issue-955
Modify call sequence to agents so all arguments are named
2 parents ac375dd + 9f2a65c commit 0414ced

File tree

12 files changed

+370
-16
lines changed

12 files changed

+370
-16
lines changed

NUnitConsole.sln

+3
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ EndProject
144144
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NUnit3.10", "src\TestData\NUnit3.10\NUnit3.10.csproj", "{0555B97D-E918-455B-951C-74EFCDA8790A}"
145145
EndProject
146146
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NUnitCommon", "NUnitCommon", "{3B30D2E5-1587-4D68-B848-1BDDB3C24BFC}"
147+
ProjectSection(SolutionItems) = preProject
148+
src\NUnitCommon\Directory.Build.props = src\NUnitCommon\Directory.Build.props
149+
EndProjectSection
147150
EndProject
148151
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "nunit.extensibility.api", "src\NUnitCommon\nunit.extensibility.api\nunit.extensibility.api.csproj", "{71DE0F2C-C72B-4CBF-99BE-F2DC0FBEDA24}"
149152
EndProject
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
2+
3+
using System;
4+
using NUnit.Engine;
5+
using NUnit.Framework;
6+
7+
namespace NUnit.Agents
8+
{
9+
public class AgentOptionTests
10+
{
11+
static TestCaseData[] DefaultSettings = new[]
12+
{
13+
new TestCaseData("AgentId", Guid.Empty),
14+
new TestCaseData("AgencyUrl", string.Empty),
15+
new TestCaseData("AgencyPid", string.Empty),
16+
new TestCaseData("DebugAgent", false),
17+
new TestCaseData("DebugTests", false),
18+
new TestCaseData("TraceLevel", InternalTraceLevel.Off),
19+
new TestCaseData("WorkDirectory", string.Empty)
20+
};
21+
22+
[TestCaseSource(nameof(DefaultSettings))]
23+
public void DefaultOptionSettings<T>(string propertyName, T defaultValue)
24+
{
25+
var options = new AgentOptions();
26+
var prop = typeof(AgentOptions).GetProperty(propertyName);
27+
Assert.That(prop, Is.Not.Null, $"Property {propertyName} does not exist");
28+
Assert.That(prop.GetValue(options, new object[0]), Is.EqualTo(defaultValue));
29+
}
30+
31+
static readonly Guid AGENT_GUID = Guid.NewGuid();
32+
static readonly TestCaseData[] ValidSettings = new[]
33+
{
34+
// Boolean options - no values provided
35+
new TestCaseData("--debug-agent", "DebugAgent", true),
36+
new TestCaseData("--debug-tests", "DebugTests", true),
37+
// Options with values - using '=' as delimiter
38+
new TestCaseData($"--agentId={AGENT_GUID}", "AgentId", AGENT_GUID),
39+
new TestCaseData("--agencyUrl=THEURL", "AgencyUrl", "THEURL"),
40+
new TestCaseData("--pid=1234", "AgencyPid", "1234"),
41+
new TestCaseData("--trace=Info", "TraceLevel", InternalTraceLevel.Info),
42+
new TestCaseData("--work=WORKDIR", "WorkDirectory", "WORKDIR"),
43+
// Options with values - using ':' as delimiter
44+
new TestCaseData("--trace:Error", "TraceLevel", InternalTraceLevel.Error),
45+
new TestCaseData("--work:WORKDIR", "WorkDirectory", "WORKDIR"),
46+
// Value with spaces (provided OS passes them through)
47+
new TestCaseData("--work:MY WORK DIR", "WorkDirectory", "MY WORK DIR"),
48+
};
49+
50+
[TestCaseSource(nameof(ValidSettings))]
51+
public void ValidOptionSettings<T>(string option, string propertyName, T expectedValue)
52+
{
53+
var options = new AgentOptions(option);
54+
var prop = typeof(AgentOptions).GetProperty(propertyName);
55+
Assert.That(prop, Is.Not.Null, $"Property {propertyName} does not exist");
56+
Assert.That(prop.GetValue(options, new object[0]), Is.EqualTo(expectedValue));
57+
}
58+
59+
[Test]
60+
public void MultipleOptions()
61+
{
62+
var options = new AgentOptions("--debug-tests", "--trace=Info", "--work", "MYWORKDIR");
63+
Assert.That(options.DebugAgent, Is.False);
64+
Assert.That(options.DebugTests);
65+
Assert.That(options.TraceLevel, Is.EqualTo(InternalTraceLevel.Info));
66+
Assert.That(options.WorkDirectory, Is.EqualTo("MYWORKDIR"));
67+
}
68+
69+
[Test]
70+
public void FileNameSupplied()
71+
{
72+
var filename = GetType().Assembly.Location;
73+
var options = new AgentOptions(filename);
74+
Assert.That(options.Files.Count, Is.EqualTo(1));
75+
Assert.That(options.Files[0], Is.EqualTo(filename));
76+
}
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
2+
3+
using NUnit.Engine;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
8+
namespace NUnit.Agents
9+
{
10+
/// <summary>
11+
/// All agents, either built-in or pluggable, must be able to
12+
/// handle the options defined in this class. In some cases,
13+
/// it may be permissible to ignore them but they should never
14+
/// give rise to an error.
15+
/// </summary>
16+
public class AgentOptions
17+
{
18+
static readonly char[] DELIMS = new[] { '=', ':' };
19+
// Dictionary containing valid options with bool value true if a value is required.
20+
static readonly Dictionary<string, bool> VALID_OPTIONS = new Dictionary<string, bool>();
21+
22+
static AgentOptions()
23+
{
24+
VALID_OPTIONS["agentId"] = true;
25+
VALID_OPTIONS["agencyUrl"] = true;
26+
VALID_OPTIONS["debug-agent"] = false;
27+
VALID_OPTIONS["debug-tests"] = false;
28+
VALID_OPTIONS["trace"] = true;
29+
VALID_OPTIONS["pid"] = true;
30+
VALID_OPTIONS["work"] = true;
31+
}
32+
33+
public AgentOptions(params string[] args)
34+
{
35+
int index;
36+
for (index = 0; index < args.Length; index++)
37+
{
38+
string arg = args[index];
39+
40+
if (IsOption(arg))
41+
{
42+
var option = arg.Substring(2);
43+
var delim = option.IndexOfAny(DELIMS);
44+
var opt = option;
45+
string? val = null;
46+
if (delim > 0)
47+
{
48+
opt = option.Substring(0, delim);
49+
val = option.Substring(delim + 1);
50+
}
51+
52+
// Simultaneously check that the option is valid and determine if it takes an argument
53+
if (!VALID_OPTIONS.TryGetValue(opt, out bool optionTakesValue))
54+
throw new Exception($"Invalid argument: {arg}");
55+
56+
if (optionTakesValue)
57+
{
58+
if (val == null && index + 1 < args.Length)
59+
val = args[++index];
60+
61+
if (val == null)
62+
throw new Exception($"Option requires a value: {arg}");
63+
}
64+
else if (delim > 0)
65+
{
66+
throw new Exception($"Option does not take a value: {arg}");
67+
}
68+
69+
if (opt == "agentId")
70+
AgentId = new Guid(GetArgumentValue(arg));
71+
else if (opt == "agencyUrl")
72+
AgencyUrl = GetArgumentValue(arg);
73+
else if (opt == "debug-agent")
74+
DebugAgent = true;
75+
else if (opt == "debug-tests")
76+
DebugTests = true;
77+
else if (opt == "trace")
78+
TraceLevel = (InternalTraceLevel)Enum.Parse(typeof(InternalTraceLevel), val.ShouldNotBeNull());
79+
else if (opt == "pid")
80+
AgencyPid = val.ShouldNotBeNull();
81+
else if (opt == "work")
82+
WorkDirectory = val.ShouldNotBeNull();
83+
else
84+
throw new Exception($"Invalid argument: {arg}");
85+
}
86+
else if (File.Exists(arg))
87+
Files.Add(arg);
88+
else
89+
throw new FileNotFoundException($"FileNotFound: {arg}");
90+
}
91+
92+
if (Files.Count > 1)
93+
throw new ArgumentException($"Only one file argument is allowed but {Files.Count} were supplied");
94+
95+
string GetArgumentValue(string argument)
96+
{
97+
var delim = argument.IndexOfAny(DELIMS);
98+
99+
if (delim > 0)
100+
return argument.Substring(delim + 1);
101+
102+
if (index + 1 < args.Length)
103+
return args[++index];
104+
105+
throw new Exception($"Option requires a value: {argument}");
106+
}
107+
}
108+
109+
public Guid AgentId { get; } = Guid.Empty;
110+
public string AgencyUrl { get; } = string.Empty;
111+
public string AgencyPid { get; } = string.Empty;
112+
public bool DebugTests { get; } = false;
113+
public bool DebugAgent { get; } = false;
114+
public InternalTraceLevel TraceLevel { get; } = InternalTraceLevel.Off;
115+
public string WorkDirectory { get; } = string.Empty;
116+
117+
public List<string> Files { get; } = new List<string>();
118+
119+
private static bool IsOption(string arg)
120+
{
121+
return arg.StartsWith("--", StringComparison.Ordinal);
122+
}
123+
}
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
2+
3+
using System;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Security;
7+
using System.Reflection;
8+
using NUnit.Engine.Agents;
9+
10+
#if NETFRAMEWORK
11+
using NUnit.Engine.Communication.Transports.Remoting;
12+
#else
13+
using NUnit.Engine.Communication.Transports.Tcp;
14+
#endif
15+
16+
namespace NUnit.Agents
17+
{
18+
public class NUnitAgent<TAgent>
19+
{
20+
static Process? AgencyProcess;
21+
static RemoteTestAgent? Agent;
22+
static readonly int _pid = Process.GetCurrentProcess().Id;
23+
static readonly Logger log = InternalTrace.GetLogger(typeof(TestAgent));
24+
25+
/// <summary>
26+
/// The main entry point for the application.
27+
/// </summary>
28+
[STAThread]
29+
public static void Execute(string[] args)
30+
{
31+
var options = new AgentOptions(args);
32+
var logName = $"nunit-agent_{_pid}.log";
33+
34+
InternalTrace.Initialize(Path.Combine(options.WorkDirectory, logName), options.TraceLevel);
35+
log.Info($"{typeof(TAgent).Name} process {_pid} starting");
36+
log.Info($" Agent Path: {Assembly.GetExecutingAssembly().Location}");
37+
38+
if (options.DebugAgent || options.DebugTests)
39+
TryLaunchDebugger();
40+
41+
log.Info($" AgentId: {options.AgentId}");
42+
log.Info($" AgencyUrl: {options.AgencyUrl}");
43+
log.Info($" AgencyPid: {options.AgencyPid}");
44+
45+
if (!string.IsNullOrEmpty(options.AgencyPid))
46+
LocateAgencyProcess(options.AgencyPid);
47+
48+
log.Info("Starting RemoteTestAgent");
49+
Agent = new RemoteTestAgent(options.AgentId);
50+
#if NETFRAMEWORK
51+
Agent.Transport = new TestAgentRemotingTransport(Agent, options.AgencyUrl);
52+
#else
53+
Agent.Transport = new TestAgentTcpTransport(Agent, options.AgencyUrl);
54+
#endif
55+
56+
try
57+
{
58+
if (Agent.Start())
59+
WaitForStop(Agent, AgencyProcess.ShouldNotBeNull());
60+
else
61+
{
62+
log.Error("Failed to start RemoteTestAgent");
63+
Environment.Exit(AgentExitCodes.FAILED_TO_START_REMOTE_AGENT);
64+
}
65+
}
66+
catch (Exception ex)
67+
{
68+
log.Error("Exception in RemoteTestAgent. {0}", ExceptionHelper.BuildMessageAndStackTrace(ex));
69+
Environment.Exit(AgentExitCodes.UNEXPECTED_EXCEPTION);
70+
}
71+
log.Info("Agent process {0} exiting cleanly", _pid);
72+
73+
Environment.Exit(AgentExitCodes.OK);
74+
}
75+
76+
private static void LocateAgencyProcess(string agencyPid)
77+
{
78+
var agencyProcessId = int.Parse(agencyPid);
79+
try
80+
{
81+
AgencyProcess = Process.GetProcessById(agencyProcessId);
82+
}
83+
catch (Exception e)
84+
{
85+
log.Error($"Unable to connect to agency process with PID: {agencyProcessId}");
86+
log.Error($"Failed with exception: {e.Message} {e.StackTrace}");
87+
Environment.Exit(AgentExitCodes.UNABLE_TO_LOCATE_AGENCY);
88+
}
89+
}
90+
91+
private static void WaitForStop(RemoteTestAgent agent, Process agencyProcess)
92+
{
93+
log.Debug("Waiting for stopSignal");
94+
95+
while (!agent.WaitForStop(500))
96+
{
97+
if (agencyProcess.HasExited)
98+
{
99+
log.Error("Parent process has been terminated.");
100+
Environment.Exit(AgentExitCodes.PARENT_PROCESS_TERMINATED);
101+
}
102+
}
103+
104+
log.Debug("Stop signal received");
105+
}
106+
107+
private static void TryLaunchDebugger()
108+
{
109+
if (Debugger.IsAttached)
110+
return;
111+
112+
try
113+
{
114+
Debugger.Launch();
115+
}
116+
catch (SecurityException se)
117+
{
118+
if (InternalTrace.Initialized)
119+
{
120+
log.Error($"System.Security.Permissions.UIPermission is not set to start the debugger. {se} {se.StackTrace}");
121+
}
122+
Environment.Exit(AgentExitCodes.DEBUGGER_SECURITY_VIOLATION);
123+
}
124+
catch (NotImplementedException nie) //Debugger is not implemented on mono
125+
{
126+
if (InternalTrace.Initialized)
127+
{
128+
log.Error($"Debugger is not available on all platforms. {nie} {nie.StackTrace}");
129+
}
130+
Environment.Exit(AgentExitCodes.DEBUGGER_NOT_IMPLEMENTED);
131+
}
132+
}
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
2+
3+
namespace NUnit.Agents
4+
{
5+
public class Net462X86Agent : NUnitAgent<Net462X86Agent>
6+
{
7+
public static void Main(string[] args) => NUnitAgent<Net462X86Agent>.Execute(args);
8+
}
9+
}

src/NUnitEngine/agents/nunit-agent-net462-x86/nunit-agent-net462-x86.csproj

+2-6
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@
2020
<Reference Include="System.Runtime.Remoting" />
2121
</ItemGroup>
2222

23-
<ItemGroup>
24-
<Compile Include="..\Program.cs" Link="Program.cs" />
25-
</ItemGroup>
26-
2723
<ItemGroup>
2824
<Content Include="..\..\..\..\nunit.ico">
2925
<Link>nunit.ico</Link>
@@ -47,9 +43,9 @@
4743
</ItemGroup>
4844

4945
<Copy SourceFiles="@(AgentFiles)" DestinationFiles="$(ConsoleDestination)%(FileName)%(Extension)" />
50-
<Message Text="Copied @(AgentFiles->Count()) files to $(ConsoleDestination)" Importance="High" />
46+
<Message Text="Copied @(AgentFiles-&gt;Count()) files to $(ConsoleDestination)" Importance="High" />
5147
<Copy SourceFiles="@(AgentFiles)" DestinationFiles="$(EngineDestination)%(FileName)%(Extension)" />
52-
<Message Text="Copied @(AgentFiles->Count()) files to $(EngineDestination)" Importance="High" />
48+
<Message Text="Copied @(AgentFiles-&gt;Count()) files to $(EngineDestination)" Importance="High" />
5349
</Target>
5450

5551
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
2+
3+
namespace NUnit.Agents
4+
{
5+
public class Net462Agent : NUnitAgent<Net462Agent>
6+
{
7+
public static void Main(string[] args) => NUnitAgent<Net462Agent>.Execute(args);
8+
}
9+
}

0 commit comments

Comments
 (0)