diff --git a/VERSIONS.txt b/VERSIONS.txt index 879efcffe..24b5576b7 100644 --- a/VERSIONS.txt +++ b/VERSIONS.txt @@ -11,7 +11,7 @@ Tizen.NET release 4.0.0 OpenTK release 3.0.1 OpenTK.GLControl release 3.0.1 MSBuild.Sdk.Extras release 2.0.54 -Cake release 0.37.0 +Cake release 0.38.4 GtkSharp release 3.22.24.37 GdkSharp release 3.22.24.37 GLibSharp release 3.22.24.37 diff --git a/build.cake b/build.cake index 38fb153f9..49cebd29c 100644 --- a/build.cake +++ b/build.cake @@ -227,6 +227,38 @@ Task ("tests") } }); +Task ("tests-wasm") + .Description ("Run WASM tests.") + .IsDependentOn ("externals-wasm") + .Does (() => +{ + var failedTests = 0; + + RunMSBuild ("./tests/SkiaSharp.Wasm.Tests.sln", + bl: $"./output/binlogs/tests-wasm.binlog"); + + var pubDir = "./tests/SkiaSharp.Wasm.Tests/bin/publish/"; + RunNetCorePublish("./tests/SkiaSharp.Wasm.Tests/SkiaSharp.Wasm.Tests.csproj", pubDir); + IProcess serverProc = null; + try { + serverProc = RunAndReturnProcess(PYTHON_EXE, new ProcessSettings { + Arguments = "server.py", + WorkingDirectory = pubDir, + }); + DotNetCoreRun("./utils/WasmTestRunner/WasmTestRunner.csproj", "http://localhost:8000/ -o ./tests/SkiaSharp.Wasm.Tests/TestResults/"); + } catch { + failedTests++; + } finally { + serverProc?.Kill(); + } + + if (failedTests > 0) + if (THROW_ON_TEST_FAILURE) + throw new Exception ($"There were {failedTests} failed tests."); + else + Warning ($"There were {failedTests} failed tests."); +}); + //////////////////////////////////////////////////////////////////////////////////////////////////// // SAMPLES - the demo apps showing off the work //////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/cake/UtilsManaged.cake b/cake/UtilsManaged.cake index e181097e9..e2d1a750d 100644 --- a/cake/UtilsManaged.cake +++ b/cake/UtilsManaged.cake @@ -66,6 +66,18 @@ void RunNetCoreTests(FilePath testAssembly) DotNetCoreTest(testAssembly.GetFilename().ToString(), settings); } +void RunNetCorePublish(FilePath testProject, DirectoryPath output) +{ + var dir = testProject.GetDirectory(); + var settings = new DotNetCorePublishSettings { + Configuration = CONFIGURATION, + NoBuild = true, + WorkingDirectory = dir, + OutputDirectory = output, + }; + DotNetCorePublish(testProject.GetFilename().ToString(), settings); +} + IEnumerable<(string Name, string Value)> CreateTraitsDictionary(string args) { if (!string.IsNullOrEmpty(args)) { diff --git a/cake/shared.cake b/cake/shared.cake index 6e87ccb3d..12e2a459a 100644 --- a/cake/shared.cake +++ b/cake/shared.cake @@ -66,6 +66,12 @@ void RunProcess(FilePath process, ProcessSettings settings) } } +IProcess RunAndReturnProcess(FilePath process, ProcessSettings settings) +{ + var proc = StartAndReturnProcess(process, settings); + return proc; +} + bool IsRunningOnMac() { return System.Environment.OSVersion.Platform == PlatformID.MacOSX || MacPlatformDetector.IsMac.Value; diff --git a/native/linux/build.cake b/native/linux/build.cake index 7d1d7e2be..29328a0bc 100644 --- a/native/linux/build.cake +++ b/native/linux/build.cake @@ -10,7 +10,7 @@ string SUPPORT_VULKAN_VAR = Argument ("supportVulkan", EnvironmentVariable ("SUP bool SUPPORT_VULKAN = SUPPORT_VULKAN_VAR == "1" || SUPPORT_VULKAN_VAR.ToLower () == "true"; string CC = Argument("cc", EnvironmentVariable("CC")); -string CXX = Argument("ccx", EnvironmentVariable("CXX")); +string CXX = Argument("cxx", EnvironmentVariable("CXX")); string AR = Argument("ar", EnvironmentVariable("AR")); string VARIANT = BUILD_VARIANT ?? "linux"; @@ -20,20 +20,20 @@ Task("libSkiaSharp") .WithCriteria(IsRunningOnLinux()) .Does(() => { + var COMPILERS = ""; + if (!string.IsNullOrEmpty(CC)) + COMPILERS += $"cc='{CC}' "; + if (!string.IsNullOrEmpty(CXX)) + COMPILERS += $"cxx='{CXX}' "; + if (!string.IsNullOrEmpty(AR)) + COMPILERS += $"ar='{AR}' "; + Build("x64", "x64", "x64"); void Build(string arch, string skiaArch, string dir) { if (Skip(arch)) return; - var compilers = ""; - if (!string.IsNullOrEmpty(CC)) - compilers += $"cc='{CC}' "; - if (!string.IsNullOrEmpty(CXX)) - compilers += $"cxx='{CXX}' "; - if (!string.IsNullOrEmpty(AR)) - compilers += $"ar='{AR}' "; - var soname = GetVersion("libSkiaSharp", "soname"); var map = MakeAbsolute((FilePath)"libSkiaSharp/libSkiaSharp.map"); @@ -55,7 +55,7 @@ Task("libSkiaSharp") $"skia_use_vulkan={SUPPORT_VULKAN} ".ToLower () + $"extra_cflags=[ '-DSKIA_C_DLL', '-DHAVE_SYSCALL_GETRANDOM', '-DXML_DEV_URANDOM' ] " + $"extra_ldflags=[ '-static-libstdc++', '-static-libgcc', '-Wl,--version-script={map}' ] " + - compilers + + COMPILERS + $"linux_soname_version='{soname}' " + ADDITIONAL_GN_ARGS); @@ -71,6 +71,12 @@ Task("libHarfBuzzSharp") .WithCriteria(IsRunningOnLinux()) .Does(() => { + var COMPILERS = ""; + if (!string.IsNullOrEmpty(CC)) + COMPILERS += $"CC='{CC}' "; + if (!string.IsNullOrEmpty(CXX)) + COMPILERS += $"CXX='{CXX}' "; + Build("x64", "x64"); void Build(string arch, string dir) @@ -80,7 +86,7 @@ Task("libHarfBuzzSharp") var soname = GetVersion("HarfBuzz", "soname"); RunProcess("make", new ProcessSettings { - Arguments = $"ARCH={arch} SONAME_VERSION={soname} VARIANT={VARIANT} LDFLAGS=-static-libstdc++", + Arguments = $"{COMPILERS} ARCH={arch} SONAME_VERSION={soname} VARIANT={VARIANT} LDFLAGS=-static-libstdc++", WorkingDirectory = "libHarfBuzzSharp", }); diff --git a/native/wasm/build.cake b/native/wasm/build.cake index 602df79d7..9cb85d64d 100644 --- a/native/wasm/build.cake +++ b/native/wasm/build.cake @@ -7,7 +7,7 @@ string SUPPORT_GPU_VAR = Argument("supportGpu", EnvironmentVariable("SUPPORT_GPU bool SUPPORT_GPU = SUPPORT_GPU_VAR == "1" || SUPPORT_GPU_VAR == "true"; string CC = Argument("cc", "emcc"); -string CXX = Argument("ccx", "em++"); +string CXX = Argument("cxx", "em++"); string AR = Argument("ar", "emar"); Task("libSkiaSharp") diff --git a/scripts/azure-pipelines.yml b/scripts/azure-pipelines.yml index 075a8acc9..93a60269c 100644 --- a/scripts/azure-pipelines.yml +++ b/scripts/azure-pipelines.yml @@ -32,6 +32,7 @@ variables: THROW_ON_TEST_FAILURE: true NUGET_DIFF_PRERELEASE: false ENABLE_CODE_COVERAGE: true + EMSCRIPTEN_VERSION: 1.39.11 resources: repositories: @@ -525,6 +526,30 @@ stages: inputs: artifactName: coverage_linux pathToPublish: 'output/coverage' + - template: azure-templates-bootstrapper.yml # Tests [WASM] (Linux) + parameters: + name: tests_wasm_linux + displayName: Tests [WASM] (Linux) + vmImage: $(VM_IMAGE_LINUX) + packages: $(MANAGED_LINUX_PACKAGES) ninja-build + target: tests-wasm + additionalArgs: --skipExternals="all" --throwOnTestFailure=$(THROW_ON_TEST_FAILURE) --coverage=false + shouldPublish: false + requiredArtifacts: + - native_wasm_linux + initScript: | + git clone https://github.com/emscripten-core/emsdk ~/emsdk + ~/emsdk/emsdk install $(EMSCRIPTEN_VERSION) + ~/emsdk/emsdk activate $(EMSCRIPTEN_VERSION) + source ~/emsdk/emsdk_env.sh + postBuildSteps: + - task: PublishTestResults@2 + displayName: Publish the WASM test results + condition: always() + inputs: + testResultsFormat: xUnit + testResultsFiles: 'tests/SkiaSharp*.Wasm.Tests/**/TestResults.xml' + testRunTitle: 'Linux WASM Tests' # TODO: add tests for linux alpine # TODO: add tests for linux no dependencies # TODO: add tests for windows nano server diff --git a/scripts/azure-templates-bootstrapper.yml b/scripts/azure-templates-bootstrapper.yml index a3d7e5425..8b4bb6cd1 100644 --- a/scripts/azure-templates-bootstrapper.yml +++ b/scripts/azure-templates-bootstrapper.yml @@ -9,6 +9,7 @@ parameters: demands: [] # the demands preBuildSteps: [] # any steps to run before the build postBuildSteps: [] # any additional steps to run after the build + initScript: '' # any scripts to run before starting the bootstrapper additionalArgs: '' # any additional arguments to pass to the bootstrapper retryCount: 1 # the number of times to retry the bootstrapper condition: succeeded() # whether or not to run this template @@ -138,7 +139,11 @@ jobs: # build - ${{ if eq(parameters.docker, '') }}: - ${{ if endsWith(parameters.name, '_windows') }}: - - pwsh: .\scripts\retry-command.ps1 -RetryCount ${{ parameters.retryCount }} { .\bootstrapper.ps1 -t ${{ parameters.target }} -v ${{ parameters.verbosity }} -c ${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} } + - pwsh: | + ${{ parameters.initScript }} + .\scripts\retry-command.ps1 -RetryCount ${{ parameters.retryCount }} { + .\bootstrapper.ps1 -t ${{ parameters.target }} -v ${{ parameters.verbosity }} -c ${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} + } env: JavaSdkDirectory: $(JAVA_HOME) LLVM_HOME: $(LLVM_HOME) @@ -146,7 +151,10 @@ jobs: AppxSymbolPackageEnabled: false displayName: Run the bootstrapper for ${{ parameters.target }} - ${{ if not(endsWith(parameters.name, '_windows')) }}: - - bash: ./scripts/retry-command.sh ${{ parameters.retryCount }} ./bootstrapper.sh -t ${{ parameters.target }} -v ${{ parameters.verbosity }} -c ${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} + - bash: | + ${{ parameters.initScript }} + ./scripts/retry-command.sh ${{ parameters.retryCount }} \ + ./bootstrapper.sh -t ${{ parameters.target }} -v ${{ parameters.verbosity }} -c ${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} env: JavaSdkDirectory: $(JAVA_HOME) displayName: Run the bootstrapper for ${{ parameters.target }} @@ -154,7 +162,10 @@ jobs: - bash: docker build --tag skiasharp . workingDirectory: ${{ parameters.docker }} displayName: Build the Docker image for ${{ parameters.docker }} - - bash: docker run --rm --name skiasharp --volume $(pwd):/work skiasharp /bin/bash scripts/retry-command.sh ${{ parameters.retryCount }} ./bootstrapper.sh -t ${{ parameters.target }} -v ${{ parameters.verbosity }} -c ${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} + - bash: | + docker run --rm --name skiasharp --volume $(pwd):/work skiasharp /bin/bash \ + scripts/retry-command.sh ${{ parameters.retryCount }} \ + ./bootstrapper.sh -t ${{ parameters.target }} -v ${{ parameters.verbosity }} -c ${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} displayName: Run the bootstrapper for ${{ parameters.target }} using the Docker image # post-build steps diff --git a/tests/SkiaSharp.Wasm.Tests.sln b/tests/SkiaSharp.Wasm.Tests.sln new file mode 100644 index 000000000..bbb36fef9 --- /dev/null +++ b/tests/SkiaSharp.Wasm.Tests.sln @@ -0,0 +1,19 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30212.25 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SkiaSharp.Wasm.Tests", "SkiaSharp.Wasm.Tests\SkiaSharp.Wasm.Tests.csproj", "{E7D44825-9F38-4ADA-BCF5-EB9420200DE4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E7D44825-9F38-4ADA-BCF5-EB9420200DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7D44825-9F38-4ADA-BCF5-EB9420200DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7D44825-9F38-4ADA-BCF5-EB9420200DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7D44825-9F38-4ADA-BCF5-EB9420200DE4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/tests/SkiaSharp.Wasm.Tests/Program.cs b/tests/SkiaSharp.Wasm.Tests/Program.cs new file mode 100644 index 000000000..16b1acc7d --- /dev/null +++ b/tests/SkiaSharp.Wasm.Tests/Program.cs @@ -0,0 +1,12 @@ +namespace SkiaSharp.Tests +{ + public class Program + { + public static int Main() + { + var testRunner = new ThreadlessXunitTestRunner(); + var result = testRunner.Run(typeof(Program).Assembly.GetName().Name + ".dll", null); + return result ? 1 : 0; + } + } +} diff --git a/tests/SkiaSharp.Wasm.Tests/SkiaSharp.Wasm.Tests.csproj b/tests/SkiaSharp.Wasm.Tests/SkiaSharp.Wasm.Tests.csproj new file mode 100644 index 000000000..4a8879577 --- /dev/null +++ b/tests/SkiaSharp.Wasm.Tests/SkiaSharp.Wasm.Tests.csproj @@ -0,0 +1,24 @@ + + + + Exe + netstandard2.0 + SkiaSharp.Tests + SkiaSharp.Tests + false + true + true + true + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/SkiaSharp.Wasm.Tests/Tests.cs b/tests/SkiaSharp.Wasm.Tests/Tests.cs new file mode 100644 index 000000000..12443473f --- /dev/null +++ b/tests/SkiaSharp.Wasm.Tests/Tests.cs @@ -0,0 +1,32 @@ +using Xunit; +using System; +using System.Runtime.InteropServices; + +namespace SkiaSharp.Tests +{ + public class PlaceholderTest + { + private const string SKIA = "libSkiaSharp"; + + [Fact] + public void CheckVersion() + { + var str = Marshal.PtrToStringAnsi(sk_version_get_string()); + var milestone = sk_version_get_milestone(); + var increment = sk_version_get_increment(); + + Assert.True(milestone > 0); + Assert.True(increment >= 0); + Assert.Equal($"{milestone}.{increment}", str); + } + + [DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr sk_version_get_string(); + + [DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)] + static extern int sk_version_get_milestone(); + + [DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)] + static extern int sk_version_get_increment(); + } +} diff --git a/tests/SkiaSharp.Wasm.Tests/WebAssemblyRuntime.cs b/tests/SkiaSharp.Wasm.Tests/WebAssemblyRuntime.cs new file mode 100644 index 000000000..e450d9ee0 --- /dev/null +++ b/tests/SkiaSharp.Wasm.Tests/WebAssemblyRuntime.cs @@ -0,0 +1,23 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace WebAssembly +{ + [Obfuscation(Feature = "renaming", Exclude = true)] + internal sealed class Runtime + { + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern string InvokeJS(string js, out int exceptionResult); + + internal static string InvokeJS(string js) + { + var r = InvokeJS(js, out var exceptionResult); + if (exceptionResult != 0) + { + Console.Error.WriteLine($"Error #{exceptionResult} \"{r}\" executing javascript: \"{js}\""); + } + return r; + } + } +} diff --git a/tests/SkiaSharp.Wasm.Tests/Xunit/ThreadlessXunitTestRunner.cs b/tests/SkiaSharp.Wasm.Tests/Xunit/ThreadlessXunitTestRunner.cs new file mode 100644 index 000000000..c2bb3b8e4 --- /dev/null +++ b/tests/SkiaSharp.Wasm.Tests/Xunit/ThreadlessXunitTestRunner.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace SkiaSharp.Tests +{ + internal class ThreadlessXunitTestRunner + { + public bool Run(string assemblyFileName, IEnumerable excludedTraits) + { + WebAssembly.Runtime.InvokeJS($"if (document) document.body.innerHTML = ''"); + + Log("Starting tests..."); + + var filters = new XunitFilters(); + foreach (var trait in excludedTraits ?? Array.Empty()) + { + 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