())
+ {
+ ParseEqualSeparatedArgument(filters.ExcludedTraits, trait);
+ }
+
+ var configuration = new TestAssemblyConfiguration
+ {
+ ShadowCopy = false,
+ ParallelizeAssembly = false,
+ ParallelizeTestCollections = false,
+ MaxParallelThreads = 1,
+ PreEnumerateTheories = false
+ };
+ var discoveryOptions = TestFrameworkOptions.ForDiscovery(configuration);
+ var discoverySink = new TestDiscoverySink();
+ var diagnosticSink = new ConsoleDiagnosticMessageSink();
+ var testOptions = TestFrameworkOptions.ForExecution(configuration);
+ var testSink = new TestMessageSink();
+ var controller = new Xunit2(
+ AppDomainSupport.Denied,
+ new NullSourceInformationProvider(),
+ assemblyFileName,
+ configFileName: null,
+ shadowCopy: false,
+ shadowCopyFolder: null,
+ diagnosticMessageSink: diagnosticSink,
+ verifyTestAssemblyExists: false);
+
+ discoveryOptions.SetSynchronousMessageReporting(true);
+ testOptions.SetSynchronousMessageReporting(true);
+
+ Log($"Discovering tests for {assemblyFileName}...");
+ var assembly = Assembly.LoadFrom(assemblyFileName);
+ var assemblyInfo = new Xunit.Sdk.ReflectionAssemblyInfo(assembly);
+ var discoverer = new ThreadlessXunitDiscoverer(assemblyInfo, new NullSourceInformationProvider(), discoverySink);
+ discoverer.FindWithoutThreads(includeSourceInformation: false, discoverySink, discoveryOptions);
+ discoverySink.Finished.WaitOne();
+ var testCasesToRun = discoverySink.TestCases.Where(filters.Filter).ToList();
+ Log($"Discovery finished.");
+ Log("");
+
+ var summarySink = new DelegatingExecutionSummarySink(
+ testSink,
+ () => false,
+ (completed, summary) => { Log($"Tests run: {summary.Total}, Errors: 0, Failures: {summary.Failed}, Skipped: {summary.Skipped}. Time: {TimeSpan.FromSeconds((double)summary.Time).TotalSeconds}s"); });
+
+ var resultsXmlAssembly = new XElement("assembly");
+ var resultsSink = new DelegatingXmlCreationSink(summarySink, resultsXmlAssembly);
+
+ testSink.Execution.TestPassedEvent += args => { Log($"[PASS] {args.Message.Test.DisplayName}", color: "green"); };
+ testSink.Execution.TestSkippedEvent += args => { Log($"[SKIP] {args.Message.Test.DisplayName}", color: "orange"); };
+ testSink.Execution.TestFailedEvent += args => { Log($"[FAIL] {args.Message.Test.DisplayName}{Environment.NewLine}{ExceptionUtility.CombineMessages(args.Message)}{Environment.NewLine}{ExceptionUtility.CombineStackTraces(args.Message)}", color: "red"); };
+
+ testSink.Execution.TestAssemblyStartingEvent += args => { Log($"Running tests for {args.Message.TestAssembly.Assembly}"); };
+ testSink.Execution.TestAssemblyFinishedEvent += args => { Log($"Finished {args.Message.TestAssembly.Assembly}{Environment.NewLine}"); };
+
+ controller.RunTests(testCasesToRun, resultsSink, testOptions);
+ resultsSink.Finished.WaitOne();
+
+ var resultsXml = new XElement("assemblies");
+ resultsXml.Add(resultsXmlAssembly);
+
+ Console.WriteLine(resultsXml.ToString());
+
+ Log("");
+ Log("Test results (Base64 encoded):");
+ var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(resultsXml.ToString()));
+ Log(base64, id: "results");
+
+ return resultsSink.ExecutionSummary.Failed > 0 || resultsSink.ExecutionSummary.Errors > 0;
+ }
+
+ private void Log(string contents, string color = null, string id = null)
+ {
+ Console.WriteLine(contents);
+
+ if (string.IsNullOrEmpty(contents))
+ contents = " ";
+
+ var ele = "";
+ if (!string.IsNullOrEmpty(id))
+ ele += $"id=\"{id}\"";
+
+ var style = "white-space: pre-wrap; word-break: break-all;";
+ if (!string.IsNullOrEmpty(color))
+ style += $"color: {color};";
+
+ WebAssembly.Runtime.InvokeJS($"if (document) document.body.innerHTML += '{contents.Replace("\n", "
")}'");
+ }
+
+ private void ParseEqualSeparatedArgument(Dictionary> targetDictionary, string argument)
+ {
+ var parts = argument.Split('=');
+ if (parts.Length != 2 || string.IsNullOrEmpty(parts[0]) || string.IsNullOrEmpty(parts[1]))
+ throw new ArgumentException("Invalid argument value '{argument}'.", nameof(argument));
+
+ var name = parts[0];
+ var value = parts[1];
+
+ List excludedTraits;
+ if (targetDictionary.TryGetValue(name, out excludedTraits!))
+ excludedTraits.Add(value);
+ else
+ targetDictionary[name] = new List { value };
+ }
+ }
+
+ internal class ThreadlessXunitDiscoverer : Xunit.Sdk.XunitTestFrameworkDiscoverer
+ {
+ public ThreadlessXunitDiscoverer(IAssemblyInfo assemblyInfo, ISourceInformationProvider sourceProvider, IMessageSink diagnosticMessageSink)
+ : base(assemblyInfo, sourceProvider, diagnosticMessageSink)
+ {
+ }
+
+ public void FindWithoutThreads(bool includeSourceInformation, IMessageSink discoveryMessageSink, ITestFrameworkDiscoveryOptions discoveryOptions)
+ {
+ using var messageBus = new Xunit.Sdk.SynchronousMessageBus(discoveryMessageSink);
+
+ foreach (var type in AssemblyInfo.GetTypes(includePrivateTypes: false).Where(IsValidTestClass))
+ {
+ var testClass = CreateTestClass(type);
+ if (!FindTestsForType(testClass, includeSourceInformation, messageBus, discoveryOptions))
+ break;
+ }
+
+ messageBus.QueueMessage(new Xunit.Sdk.DiscoveryCompleteMessage());
+ }
+ }
+
+ internal class ConsoleDiagnosticMessageSink : Xunit.Sdk.LongLivedMarshalByRefObject, IMessageSink
+ {
+ public bool OnMessage(IMessageSinkMessage message)
+ {
+ if (message is IDiagnosticMessage diagnosticMessage)
+ Console.WriteLine(diagnosticMessage.Message);
+
+ return true;
+ }
+ }
+}
diff --git a/tools/packages.config b/tools/packages.config
index cedcc6ab5..a36acb6e5 100644
--- a/tools/packages.config
+++ b/tools/packages.config
@@ -1,4 +1,4 @@
-
+
diff --git a/utils/NativeLibraryMiniTest/linux/build.sh b/utils/NativeLibraryMiniTest/linux/build.sh
index 353db3e2d..2e4ea437f 100644
--- a/utils/NativeLibraryMiniTest/linux/build.sh
+++ b/utils/NativeLibraryMiniTest/linux/build.sh
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
-mkdir -p utils/NativeLibraryMiniTest/bin
-csc /out:utils/NativeLibraryMiniTest/bin/Program.exe /unsafe utils/NativeLibraryMiniTest/Program.cs
-cp output/native/linux/x64/libSkiaSharp.so utils/NativeLibraryMiniTest/bin/
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
-(cd utils/NativeLibraryMiniTest/bin && mono Program.exe)
+mkdir -p $DIR/bin
+csc /out:$DIR/bin/Program.exe /unsafe $DIR/../source/Program.cs
+cp $DIR/../../../output/native/linux/x64/libSkiaSharp.so $DIR/bin/
+
+(cd $DIR/bin && mono Program.exe)
diff --git a/utils/NativeLibraryMiniTest/source/Program.cs b/utils/NativeLibraryMiniTest/source/Program.cs
index aea5f18d8..0a708704c 100644
--- a/utils/NativeLibraryMiniTest/source/Program.cs
+++ b/utils/NativeLibraryMiniTest/source/Program.cs
@@ -15,7 +15,9 @@ namespace NativeLibraryMiniTest {
Console.WriteLine("Starting test...");
Console.WriteLine("Version test...");
- Console.WriteLine($"sk_version_get_string() = {sk_version_get_string()}");
+ Console.WriteLine($"sk_version_get_milestone() = {sk_version_get_milestone()}");
+ var str = Marshal.PtrToStringAnsi((IntPtr)sk_version_get_string());
+ Console.WriteLine($"sk_version_get_string() = {str}");
Console.WriteLine("Color type test...");
Console.WriteLine($"sk_colortype_get_default_8888() = {sk_colortype_get_default_8888()}");
@@ -48,7 +50,10 @@ namespace NativeLibraryMiniTest {
[DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.LPStr)]
- static extern string sk_version_get_string();
+ static extern void* sk_version_get_string();
+
+ [DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
+ static extern int sk_version_get_milestone();
[DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
static extern sk_colortype_t sk_colortype_get_default_8888();
diff --git a/utils/NativeLibraryMiniTest/wasm/build.sh b/utils/NativeLibraryMiniTest/wasm/build.sh
index 785a7cdb3..a52ac9046 100644
--- a/utils/NativeLibraryMiniTest/wasm/build.sh
+++ b/utils/NativeLibraryMiniTest/wasm/build.sh
@@ -1,4 +1,6 @@
#!/usr/bin/env bash
-msbuild /r /bl utils/NativeLibraryMiniTest/wasm
-(cd utils/NativeLibraryMiniTest/wasm/bin/publish && python3 server.py)
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+
+msbuild /r /bl $DIR
+(cd $DIR/bin/publish && python3 server.py)
diff --git a/utils/README.md b/utils/README.md
index e876ca622..be3f10c74 100644
--- a/utils/README.md
+++ b/utils/README.md
@@ -35,3 +35,17 @@ dotnet run --project=utils/SkiaSharpGenerator/SkiaSharpGenerator.csproj -- verif
The path to the root of the skia source.
* `--output binding/Binding/SkiaApi.generated.cs`
The path to the generated file.
+
+## WasmTestRunner
+
+Run the WASM unit tests in a browser.
+
+This can be run with:
+
+```pwsh
+dotnet run --project=utils/WasmTestRunner/WasmTestRunner.csproj -- "http://localhost:5000/"
+```
+
+* `--output TestResults.xml`
+* `--timeout 30`
+* `--no-headless`
diff --git a/utils/Utils.sln b/utils/Utils.sln
index 266a0b452..2062c2822 100644
--- a/utils/Utils.sln
+++ b/utils/Utils.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.29423.271
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpGenerator", "SkiaSharpGenerator\SkiaSharpGenerator.csproj", "{970EA255-F11F-4551-AEC4-6666C1192259}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmTestRunner", "WasmTestRunner\WasmTestRunner.csproj", "{7C7ED740-A8D2-46BE-97A0-0F8EF33833D0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{970EA255-F11F-4551-AEC4-6666C1192259}.Debug|Any CPU.Build.0 = Debug|Any CPU
{970EA255-F11F-4551-AEC4-6666C1192259}.Release|Any CPU.ActiveCfg = Release|Any CPU
{970EA255-F11F-4551-AEC4-6666C1192259}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C7ED740-A8D2-46BE-97A0-0F8EF33833D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C7ED740-A8D2-46BE-97A0-0F8EF33833D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C7ED740-A8D2-46BE-97A0-0F8EF33833D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C7ED740-A8D2-46BE-97A0-0F8EF33833D0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/utils/WasmTestRunner/Program.cs b/utils/WasmTestRunner/Program.cs
new file mode 100644
index 000000000..1638ab4ab
--- /dev/null
+++ b/utils/WasmTestRunner/Program.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Xml.Linq;
+using Mono.Options;
+using OpenQA.Selenium.Chrome;
+
+namespace WasmTestRunner
+{
+ public class Program
+ {
+ private const string DefaultUrl = "http://localhost:5000/";
+ private const string ResultsFileName = "TestResults.xml";
+
+ public static string OutputPath { get; set; } = Directory.GetCurrentDirectory();
+
+ public static int Timeout { get; set; } = 30;
+
+ public static bool UseHeadless { get; set; } = true;
+
+ public static bool ShowHelp { get; set; }
+
+ public static bool Verbose { get; set; }
+
+ public static string Url { get; set; } = DefaultUrl;
+
+ public static int Main(string[] args)
+ {
+ var p = new OptionSet
+ {
+ { "o|output=", "the path to the test results file. Default is the current directory.", v => OutputPath = v },
+ { "t|timeout=", "the number of seconds to wait before timing out. Default is 30.", (int v) => Timeout = v },
+ { "no-headless", "do not use a headless browser.", v => UseHeadless = false },
+ { "v|verbose", "show verbose error messages.", v => Verbose= true },
+ { "h|help", "show this message and exit.", v => ShowHelp = true },
+ };
+
+ List extra;
+ try
+ {
+ extra = p.Parse(args);
+
+ if (extra.Count > 1)
+ throw new OptionException();
+
+ Url = extra.FirstOrDefault() ?? DefaultUrl;
+ if (string.IsNullOrEmpty(OutputPath))
+ OutputPath = Directory.GetCurrentDirectory();
+ OutputPath = Path.Combine(OutputPath, ResultsFileName);
+ var dir = Path.GetDirectoryName(OutputPath);
+ if (!string.IsNullOrEmpty(dir))
+ Directory.CreateDirectory(dir);
+ }
+ catch (OptionException e)
+ {
+ Console.Error.Write("wasm-test: ");
+ Console.Error.WriteLine(e.Message);
+ Console.Error.WriteLine("Try `wasm-test --help' for more information.");
+
+ return 1;
+ }
+
+ if (ShowHelp)
+ {
+ Console.WriteLine("Usage: wasm-test [OPTIONS]+ URL");
+ Console.WriteLine("Run WASM tests in Chrome.");
+ Console.WriteLine();
+ Console.WriteLine("Options:");
+ p.WriteOptionDescriptions(Console.Out);
+
+ return 0;
+ }
+
+ try
+ {
+ RunTests();
+
+ var xdoc = XDocument.Load(OutputPath);
+ var failed = xdoc.Root.Element("assembly").Attribute("failed").Value;
+ if (failed != "0")
+ throw new Exception($"There were test failures: {failed}");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"There was an error running the tests: {ex.Message}");
+ if (Verbose)
+ Console.Error.WriteLine(ex);
+
+ return 1;
+ }
+
+ return 0;
+ }
+
+ private static void RunTests()
+ {
+ var options = new ChromeOptions();
+ if (UseHeadless)
+ {
+ options.AddArgument("no-sandbox");
+ options.AddArgument("headless");
+ }
+
+ options.AddArgument("window-size=1024x768");
+
+ using var service = ChromeDriverService.CreateDefaultService();
+ using var driver = new ChromeDriver(service, options);
+
+ driver.Url = Url;
+
+ var index = 0;
+ var currentTimeout = Timeout;
+
+ do
+ {
+ var pre = driver.FindElementsByTagName("PRE").Skip(index).ToArray();
+ if (pre.Length > 0)
+ {
+ index += pre.Length;
+ currentTimeout = Timeout; // reset the timeout
+
+ foreach (var e in pre)
+ Console.WriteLine(e.Text);
+ }
+
+ var resultsElement = driver.FindElementsById("results");
+ if (resultsElement.Count == 0)
+ {
+ if (driver.FindElementsByClassName("neterror").Count > 0)
+ {
+ var errorCode = driver.FindElementsByClassName("error-code").FirstOrDefault()?.Text;
+ throw new Exception($"There was an error loading the page: {errorCode}");
+ }
+
+ Thread.Sleep(500);
+ continue;
+ }
+
+ var text = resultsElement[0].Text;
+ var bytes = Convert.FromBase64String(text);
+ File.WriteAllBytes(OutputPath, bytes);
+ break;
+ } while (--currentTimeout > 0);
+
+ if (currentTimeout <= 0)
+ throw new TimeoutException();
+ }
+ }
+}
diff --git a/utils/WasmTestRunner/WasmTestRunner.csproj b/utils/WasmTestRunner/WasmTestRunner.csproj
new file mode 100644
index 000000000..61f05c6f2
--- /dev/null
+++ b/utils/WasmTestRunner/WasmTestRunner.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ netcoreapp3.0
+ 8.0
+ wasm-test
+
+
+
+
+
+
+
+
+
\ No newline at end of file