Skip to content

Commit a54eccd

Browse files
authored
Add the WASI runtime test target (#977)
Add the WASI runtime test target with Wasmtime engine
1 parent 0ab74a4 commit a54eccd

File tree

14 files changed

+306
-29
lines changed

14 files changed

+306
-29
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This repo contains the code to build the **XHarness dotnet tool** and a **TestRu
44

55
## What is XHarness
66

7-
XHarness is primarily a command line tool that enables running xUnit like tests on Android, Apple iOS / tvOS / WatchOS / Mac Catalyst and desktop browsers (WASM).
7+
XHarness is primarily a command line tool that enables running xUnit like tests on Android, Apple iOS / tvOS / WatchOS / Mac Catalyst, WASI and desktop browsers (WASM).
88
It can
99
- locate devices/emulators
1010
- install a given application, run it and collect results uninstalling it after,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
7+
namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasi;
8+
9+
internal class WasmEngineArgument : Argument<WasmEngine?>
10+
{
11+
public WasmEngineArgument()
12+
: base("engine=|e=", "Specifies the Wasm engine to be used", null)
13+
{
14+
}
15+
16+
public override void Action(string argumentValue) =>
17+
Value = ParseArgument<WasmEngine>("engine", argumentValue);
18+
19+
// Set WasmTime as default engine
20+
public override void Validate() => Value ??= WasmEngine.WasmTime;
21+
}
22+
23+
/// <summary>
24+
/// Specifies a name of a wasm engine used to run WASI application.
25+
/// </summary>
26+
internal enum WasmEngine
27+
{
28+
/// <summary>
29+
/// WasmTime
30+
/// </summary>
31+
WasmTime,
32+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasi;
6+
7+
internal class WasmEngineArguments : RepeatableArgument
8+
{
9+
public WasmEngineArguments()
10+
: base("engine-arg=", "Argument to pass to the wasm engine")
11+
{
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasi;
6+
7+
internal class WasmEngineLocationArgument : StringArgument
8+
{
9+
public WasmEngineLocationArgument() : base("wasm-engine-path=", "Path to the wasm engine to be used. This must correspond to the engine specified with -e")
10+
{
11+
}
12+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
8+
namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasi;
9+
10+
internal class WasiTestCommandArguments : XHarnessCommandArguments
11+
{
12+
public WasmEngineArgument Engine { get; } = new();
13+
public WasmEngineLocationArgument EnginePath { get; } = new();
14+
public WasmEngineArguments EngineArgs { get; } = new();
15+
public ExpectedExitCodeArgument ExpectedExitCode { get; } = new((int)Common.CLI.ExitCode.SUCCESS);
16+
public OutputDirectoryArgument OutputDirectory { get; } = new();
17+
public TimeoutArgument Timeout { get; } = new(TimeSpan.FromMinutes(15));
18+
19+
protected override IEnumerable<Argument> GetArguments() => new Argument[]
20+
{
21+
Engine,
22+
EnginePath,
23+
EngineArgs,
24+
OutputDirectory,
25+
Timeout,
26+
ExpectedExitCode,
27+
};
28+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.ComponentModel;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Runtime.InteropServices;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasi;
14+
using Microsoft.DotNet.XHarness.CLI.Commands.Wasm;
15+
using Microsoft.DotNet.XHarness.Common;
16+
using Microsoft.DotNet.XHarness.Common.CLI;
17+
using Microsoft.DotNet.XHarness.Common.Execution;
18+
using Microsoft.DotNet.XHarness.Common.Logging;
19+
using Microsoft.DotNet.XHarness.Common.Utilities;
20+
using Microsoft.Extensions.DependencyInjection;
21+
using Microsoft.Extensions.Logging;
22+
23+
namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasi;
24+
internal class WasiTestCommand : XHarnessCommand<WasiTestCommandArguments>
25+
{
26+
private const string CommandHelp = "Executes tests on WASI using a selected engine";
27+
28+
protected override WasiTestCommandArguments Arguments { get; } = new();
29+
protected override string CommandUsage { get; } = "wasi test [OPTIONS] -- [ENGINE OPTIONS]";
30+
protected override string CommandDescription { get; } = CommandHelp;
31+
32+
public WasiTestCommand() : base(TargetPlatform.WASI, "test", true, new ServiceCollection(), CommandHelp)
33+
{
34+
}
35+
36+
protected override async Task<ExitCode> InvokeInternal(ILogger logger)
37+
{
38+
var processManager = ProcessManagerFactory.CreateProcessManager();
39+
40+
string engineBinary = Arguments.Engine.Value switch
41+
{
42+
WasmEngine.WasmTime => "wasmtime",
43+
_ => throw new ArgumentException("Engine not set")
44+
};
45+
46+
if (!string.IsNullOrEmpty(Arguments.EnginePath.Value))
47+
{
48+
engineBinary = Arguments.EnginePath.Value;
49+
if (Path.IsPathRooted(engineBinary) && !File.Exists(engineBinary))
50+
throw new ArgumentException($"Could not find js engine at the specified path - {engineBinary}");
51+
}
52+
else
53+
{
54+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
55+
{
56+
engineBinary = FileUtils.FindFileInPath(engineBinary + ".cmd");
57+
}
58+
}
59+
60+
var cts = new CancellationTokenSource();
61+
try
62+
{
63+
logger.LogInformation($"Using wasm engine {Arguments.Engine.Value} from path {engineBinary}");
64+
await PrintVersionAsync(Arguments.Engine.Value.Value, engineBinary);
65+
66+
var engineArgs = new List<string>();
67+
engineArgs.AddRange(Arguments.EngineArgs.Value);
68+
engineArgs.AddRange(PassThroughArguments);
69+
70+
var xmlResultsFilePath = Path.Combine(Arguments.OutputDirectory, "testResults.xml");
71+
File.Delete(xmlResultsFilePath);
72+
73+
var stdoutFilePath = Path.Combine(Arguments.OutputDirectory, "wasi-console.log");
74+
File.Delete(stdoutFilePath);
75+
76+
var logProcessor = new WasmTestMessagesProcessor(xmlResultsFilePath,
77+
stdoutFilePath,
78+
logger);
79+
var logProcessorTask = Task.Run(() => logProcessor.RunAsync(cts.Token));
80+
81+
var processTask = processManager.ExecuteCommandAsync(
82+
engineBinary,
83+
engineArgs,
84+
log: new CallbackLog(m => logger.LogInformation(m.Trim())),
85+
stdoutLog: new CallbackLog(msg => logProcessor.Invoke(msg)),
86+
stderrLog: new CallbackLog(logProcessor.ProcessErrorMessage),
87+
Arguments.Timeout);
88+
89+
var tasks = new Task[]
90+
{
91+
logProcessorTask,
92+
processTask,
93+
Task.Delay(Arguments.Timeout)
94+
};
95+
96+
var task = await Task.WhenAny(tasks).ConfigureAwait(false);
97+
if (task == tasks[^1] || cts.IsCancellationRequested || task.IsCanceled)
98+
{
99+
logger.LogError($"Tests timed out after {((TimeSpan)Arguments.Timeout).TotalSeconds}secs");
100+
if (!cts.IsCancellationRequested)
101+
cts.Cancel();
102+
103+
return ExitCode.TIMED_OUT;
104+
}
105+
106+
if (task.IsFaulted)
107+
{
108+
logger.LogError($"task faulted {task.Exception}");
109+
throw task.Exception!;
110+
}
111+
112+
// if the log processor completed without errors, then the
113+
// process should be done too, or about to be done!
114+
var result = await processTask;
115+
ExitCode logProcessorExitCode = await logProcessor.CompleteAndFlushAsync();
116+
117+
if (result.ExitCode != Arguments.ExpectedExitCode)
118+
{
119+
logger.LogError($"Application has finished with exit code {result.ExitCode} but {Arguments.ExpectedExitCode} was expected");
120+
return ExitCode.GENERAL_FAILURE;
121+
}
122+
else
123+
{
124+
logger.LogInformation("Application has finished with exit code: " + result.ExitCode);
125+
// return SUCCESS if logProcess also returned SUCCESS
126+
return logProcessorExitCode;
127+
}
128+
}
129+
catch (Win32Exception e) when (e.NativeErrorCode == 2)
130+
{
131+
logger.LogCritical($"The engine binary `{engineBinary}` was not found");
132+
return ExitCode.APP_LAUNCH_FAILURE;
133+
}
134+
finally
135+
{
136+
if (!cts.IsCancellationRequested)
137+
{
138+
cts.Cancel();
139+
}
140+
}
141+
142+
Task PrintVersionAsync(WasmEngine engine, string engineBinary)
143+
{
144+
return processManager.ExecuteCommandAsync(
145+
engineBinary,
146+
new[] { "--version" },
147+
log: new CallbackLog(m => logger.LogDebug(m.Trim())),
148+
stdoutLog: new CallbackLog(msg => logger.LogInformation(msg.Trim())),
149+
stderrLog: new CallbackLog(msg => logger.LogError(msg.Trim())),
150+
TimeSpan.FromSeconds(10));
151+
}
152+
}
153+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Mono.Options;
6+
7+
namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasi;
8+
9+
// Main WASI command set that contains the platform specific commands. This allows the command line to
10+
// support different options in different platforms.
11+
// Whenever the behavior does match, the goal is to have the same arguments for all platforms
12+
public class WasiCommandSet : CommandSet
13+
{
14+
public WasiCommandSet() : base("wasi")
15+
{
16+
Add(new WasiTestCommand());
17+
}
18+
}

src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/JS/WasmTestCommand.cs

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Microsoft.DotNet.XHarness.Common.CLI;
1616
using Microsoft.DotNet.XHarness.Common.Execution;
1717
using Microsoft.DotNet.XHarness.Common.Logging;
18+
using Microsoft.DotNet.XHarness.Common.Utilities;
1819
using Microsoft.Extensions.DependencyInjection;
1920
using Microsoft.Extensions.Logging;
2021

@@ -30,27 +31,7 @@ internal class WasmTestCommand : XHarnessCommand<WasmTestCommandArguments>
3031

3132
public WasmTestCommand() : base(TargetPlatform.WASM, "test", true, new ServiceCollection(), CommandHelp)
3233
{
33-
}
34-
35-
private static string FindEngineInPath(string engineBinary)
36-
{
37-
if (File.Exists(engineBinary) || Path.IsPathRooted(engineBinary))
38-
return engineBinary;
39-
40-
var path = Environment.GetEnvironmentVariable("PATH");
41-
42-
if (path == null)
43-
return engineBinary;
44-
45-
foreach (var folder in path.Split(Path.PathSeparator))
46-
{
47-
var fullPath = Path.Combine(folder, engineBinary);
48-
if (File.Exists(fullPath))
49-
return fullPath;
50-
}
51-
52-
return engineBinary;
53-
}
34+
}
5435

5536
protected override async Task<ExitCode> InvokeInternal(ILogger logger)
5637
{
@@ -76,18 +57,18 @@ protected override async Task<ExitCode> InvokeInternal(ILogger logger)
7657
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
7758
{
7859
if (engineBinary.Equals("node"))
79-
engineBinary = FindEngineInPath(engineBinary + ".exe"); // NodeJS ships as .exe rather than .cmd
60+
engineBinary = FileUtils.FindFileInPath(engineBinary + ".exe"); // NodeJS ships as .exe rather than .cmd
8061
else
81-
engineBinary = FindEngineInPath(engineBinary + ".cmd");
62+
engineBinary = FileUtils.FindFileInPath(engineBinary + ".cmd");
8263
}
8364
}
8465

85-
logger.LogInformation($"Using js engine {Arguments.Engine.Value} from path {engineBinary}");
86-
await PrintVersionAsync(Arguments.Engine.Value.Value, engineBinary);
87-
8866
var cts = new CancellationTokenSource();
8967
try
9068
{
69+
logger.LogInformation($"Using js engine {Arguments.Engine.Value} from path {engineBinary}");
70+
await PrintVersionAsync(Arguments.Engine.Value.Value, engineBinary);
71+
9172
ServerURLs? serverURLs = null;
9273
if (Arguments.WebServerMiddlewarePathsAndTypes.Value.Count > 0)
9374
{

src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/WasmTestMessagesProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class WasmTestMessagesProcessor
3838
// Set once `WASM EXIT` message is received
3939
public TaskCompletionSource WasmExitReceivedTcs { get; } = new ();
4040

41-
public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile, WasmSymbolicatorBase? symbolicator)
41+
public WasmTestMessagesProcessor(string xmlResultsFilePath, string stdoutFilePath, ILogger logger, string? errorPatternsFile = null, WasmSymbolicatorBase? symbolicator = null)
4242
{
4343
_xmlResultsFilePath = xmlResultsFilePath;
4444
_stdoutFileWriter = File.CreateText(stdoutFilePath);

src/Microsoft.DotNet.XHarness.CLI/Commands/XHarnessHelpCommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.DotNet.XHarness.CLI.Commands.Apple;
1111
using Microsoft.DotNet.XHarness.CLI.Commands.Apple.Simulators;
1212
using Microsoft.DotNet.XHarness.CLI.Commands.Wasm;
13+
using Microsoft.DotNet.XHarness.CLI.Commands.Wasi;
1314
using Microsoft.DotNet.XHarness.Common.CLI;
1415
using Mono.Options;
1516

@@ -67,8 +68,11 @@ public override int Invoke(IEnumerable<string> arguments)
6768
case "wasm":
6869
PrintCommandHelp(new WasmCommandSet(), subCommand);
6970
break;
71+
case "wasi":
72+
PrintCommandHelp(new WasiCommandSet(), subCommand);
73+
break;
7074
default:
71-
Console.WriteLine($"No help available for command '{command}'. Allowed commands are 'apple', 'wasm' and 'android'");
75+
Console.WriteLine($"No help available for command '{command}'. Allowed commands are 'apple', 'wasm', 'wasi' and 'android'");
7276
break;
7377
}
7478

0 commit comments

Comments
 (0)