WIP: Cross-platform app tests

This commit is contained in:
Ben Olden-Cooligan 2022-12-18 13:52:33 -08:00
parent 323d9b0a4c
commit a0a012b7f2
19 changed files with 228 additions and 62 deletions

View File

@ -0,0 +1,28 @@
using System.Collections;
using NAPS2.App.Tests.Targets;
namespace NAPS2.App.Tests;
public class AppTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
if (OperatingSystem.IsWindows())
{
yield return new object[] { new WinNet462AppTestTarget() };
}
else if (OperatingSystem.IsMacOS())
{
yield return new object[] { new MacAppTestTarget() };
}
else if (OperatingSystem.IsLinux())
{
yield return new object[] { new LinuxAppTestTarget() };
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -1,31 +1,34 @@
using System.Runtime.InteropServices;
using System.Threading;
using NAPS2.App.Tests.Targets;
using Xunit;
namespace NAPS2.App.Tests;
public static class AppTestHelper
{
public static Process StartGuiProcess(string exeName, string appData, string args = null)
public static Process StartGuiProcess(AppTestExe exe, string appData, string args = null)
{
var startInfo = GetProcessStartInfo(exeName, appData, args);
var startInfo = GetProcessStartInfo(exe, appData, args);
return Process.Start(startInfo);
}
public static Process StartProcess(string exeName, string appData, string args = null)
public static Process StartProcess(AppTestExe exe, string appData, string args = null)
{
var startInfo = GetProcessStartInfo(exeName, appData, args);
var startInfo = GetProcessStartInfo(exe, appData, args);
startInfo.RedirectStandardInput = true;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
return Process.Start(startInfo);
}
private static ProcessStartInfo GetProcessStartInfo(string exeName, string appData, string args) =>
private static ProcessStartInfo GetProcessStartInfo(AppTestExe exe, string appData, string args) =>
new()
{
FileName = GetExePath(exeName),
Arguments = args ?? "",
FileName = GetExePath(exe),
Arguments = exe.ArgPrefix != null && args != null
? $"{exe.ArgPrefix} {args}"
: exe.ArgPrefix ?? args ?? "",
UseShellExecute = false,
EnvironmentVariables =
{
@ -33,24 +36,24 @@ public static class AppTestHelper
}
};
public static string GetBaseDirectory()
public static string GetBaseDirectory(AppTestExe exe)
{
var envDirectory = Environment.GetEnvironmentVariable("NAPS2_TEST_ROOT");
var testDirectory = AssemblyHelper.LibFolder;
var testDirectory = exe.DefaultRootPath;
return string.IsNullOrEmpty(envDirectory) ? testDirectory : envDirectory;
}
public static string GetExePath(string exeName)
public static string GetExePath(AppTestExe exe)
{
var dir = GetBaseDirectory();
var file = Path.Combine(dir, exeName);
if (!File.Exists(file))
var dir = GetBaseDirectory(exe);
if (dir != exe.DefaultRootPath)
{
file = Path.Combine(dir, "lib", exeName);
dir = Path.Combine(dir, exe.TestRootSubPath);
}
var file = Path.Combine(dir, exe.ExeSubPath);
if (!File.Exists(file))
{
throw new Exception($"Could not find {exeName} in {dir}");
throw new Exception($"Could not find {exe.ExeSubPath} in {dir}");
}
return file;
}
@ -98,4 +101,6 @@ public static class AppTestHelper
var path = Path.Combine(appData, "errorlog.txt");
Assert.True(File.Exists(path), path);
}
public static string SolutionRoot => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
}

View File

@ -0,0 +1,28 @@
using System.Collections;
using NAPS2.App.Tests.Targets;
namespace NAPS2.App.Tests.Appium;
public class AppiumTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
if (OperatingSystem.IsWindows())
{
yield return new object[] { new WinNet462AppTestTarget() };
}
else if (OperatingSystem.IsMacOS())
{
// No Appium impl yet
}
else if (OperatingSystem.IsLinux())
{
// No Appium impl yet
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -1,4 +1,5 @@
using System.Threading;
using NAPS2.App.Tests.Targets;
using NAPS2.Sdk.Tests;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
@ -8,19 +9,19 @@ namespace NAPS2.App.Tests.Appium;
public class AppiumTests : ContextualTests
{
protected readonly WindowsDriver<WindowsElement> _session;
protected WindowsDriver<WindowsElement> _session;
private static WindowsDriver<WindowsElement> StartSession(string exeName, string appData)
private static WindowsDriver<WindowsElement> StartSession(AppTestExe exe, string appData)
{
var opts = new AppiumOptions();
opts.AddAdditionalCapability("app", AppTestHelper.GetExePath(exeName));
opts.AddAdditionalCapability("app", AppTestHelper.GetExePath(exe));
opts.AddAdditionalCapability("appArguments", $"/Naps2TestData \"{appData}\"");
return new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), opts);
}
public AppiumTests()
public void Init(IAppTestTarget target)
{
_session = StartSession("NAPS2.exe", FolderPath);
_session = StartSession(target.Gui, FolderPath);
}
public override void Dispose()

View File

@ -1,4 +1,5 @@
using System.Threading;
using NAPS2.App.Tests.Targets;
using NAPS2.App.Tests.Verification;
using NAPS2.Sdk.Tests;
using NAPS2.Sdk.Tests.Asserts;
@ -9,9 +10,11 @@ namespace NAPS2.App.Tests.Appium;
[Collection("appium")]
public class ImportAndSaveTests : AppiumTests
{
[VerifyFact(AllowDebug = true)]
public void ImportVariousAndSavePdfWithOcr()
[VerifyTheory(AllowDebug = true)]
[ClassData(typeof(AppiumTestData))]
public void ImportVariousAndSavePdfWithOcr(IAppTestTarget target)
{
Init(target);
CopyResourceToFile(PdfResources.word_generated_pdf, "word.pdf");
CopyResourceToFile(PdfResources.word_patcht_pdf, "patcht.pdf");
CopyResourceToFile(PdfResources.image_pdf, "image.pdf");

View File

@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using NAPS2.App.Tests.Targets;
using NAPS2.App.Tests.Verification;
using OpenQA.Selenium.Appium.Windows;
using Xunit;
@ -11,9 +12,11 @@ public class LanguageSelectionTests : AppiumTests
{
private static readonly HashSet<string> ExpectedMissingLanguages = new() { "bn", "hi", "id", "th", "ur" };
[VerifyFact(AllowDebug = true)]
public void OpenLanguageDropdown()
[VerifyTheory(AllowDebug = true)]
[ClassData(typeof(AppiumTestData))]
public void OpenLanguageDropdown(IAppTestTarget target)
{
Init(target);
// Open the Language dropdown
ClickAtName("Language");
var menuItems = GetMenuItems();

View File

@ -1,4 +1,5 @@
using System.Threading;
using NAPS2.App.Tests.Targets;
using NAPS2.App.Tests.Verification;
using NAPS2.Sdk.Tests.Asserts;
using Xunit;
@ -11,9 +12,11 @@ public class ScanAndSaveTests : AppiumTests
private const string WIA_DEVICE_NAME = "";
private const string TWAIN_DEVICE_NAME = "Canon MP495 ser";
[VerifyFact(AllowDebug = true)]
public void ScanWiaSavePdf()
[VerifyTheory(AllowDebug = true)]
[ClassData(typeof(AppiumTestData))]
public void ScanWiaSavePdf(IAppTestTarget target)
{
Init(target);
// Clicking Scan without a profile opens the profile settings window
ClickAtName("Scan");
// WIA driver is selected by default, so we open the WIA device dialog
@ -44,9 +47,11 @@ public class ScanAndSaveTests : AppiumTests
AppTestHelper.AssertNoErrorLog(FolderPath);
}
[VerifyFact(AllowDebug = true)]
public void ScanTwainSaveImage()
[VerifyTheory(AllowDebug = true)]
[ClassData(typeof(AppiumTestData))]
public void ScanTwainSaveImage(IAppTestTarget target)
{
Init(target);
// Clicking Scan without a profile opens the profile settings window
ClickAtName("Scan");
ClickAtName("TWAIN Driver");

View File

@ -1,3 +1,4 @@
using NAPS2.App.Tests.Targets;
using NAPS2.Sdk.Tests;
using Xunit;
@ -5,14 +6,15 @@ namespace NAPS2.App.Tests;
public class ConsoleAppTests : ContextualTests
{
[Fact]
public void ConvertsImportedFile()
[Theory]
[ClassData(typeof(AppTestData))]
public void ConvertsImportedFile(IAppTestTarget target)
{
var importPath = CopyResourceToFile(ImageResources.dog, "in.png");
var outputPath = Path.Combine(FolderPath, "out.jpg");
var args = $"-n 0 -i \"{importPath}\" -o \"{outputPath}\"";
var process = AppTestHelper.StartProcess("NAPS2.Console.exe", FolderPath, args);
var process = AppTestHelper.StartProcess(target.Console, FolderPath, args);
try
{
Assert.True(process.WaitForExit(5000));
@ -28,14 +30,15 @@ public class ConsoleAppTests : ContextualTests
}
}
[Fact]
public void NonZeroExitCodeForError()
[Theory]
[ClassData(typeof(AppTestData))]
public void NonZeroExitCodeForError(IAppTestTarget target)
{
var importPath = Path.Combine(FolderPath, "doesnotexist.png");
var outputPath = Path.Combine(FolderPath, "out.jpg");
var args = $"-n 0 -i \"{importPath}\" -o \"{outputPath}\"";
var process = AppTestHelper.StartProcess("NAPS2.Console.exe", FolderPath, args);
var process = AppTestHelper.StartProcess(target.Console, FolderPath, args);
try
{
Assert.True(process.WaitForExit(5000));

View File

@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">net6-windows;net462</TargetFrameworks>
<TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('Windows'))">net6</TargetFrameworks>
<TargetFramework>net6</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
<Configurations>Debug;Release;DebugLang</Configurations>
@ -25,11 +24,4 @@
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<ProjectReference Include="..\NAPS2.App.Console\NAPS2.App.Console.csproj" />
<ProjectReference Include="..\NAPS2.App.PortableLauncher\NAPS2.App.PortableLauncher.csproj" />
<ProjectReference Include="..\NAPS2.App.WinForms\NAPS2.App.WinForms.csproj" />
<ProjectReference Include="..\NAPS2.App.Worker\NAPS2.App.Worker.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,3 @@
namespace NAPS2.App.Tests.Targets;
public record AppTestExe(string DefaultRootPath, string ExeSubPath, string ArgPrefix = null, string TestRootSubPath = null);

View File

@ -0,0 +1,8 @@
namespace NAPS2.App.Tests.Targets;
public interface IAppTestTarget
{
AppTestExe Console { get; }
AppTestExe Gui { get; }
AppTestExe Worker { get; }
}

View File

@ -0,0 +1,18 @@
namespace NAPS2.App.Tests.Targets;
public class LinuxAppTestTarget : IAppTestTarget
{
public AppTestExe Console => GetAppTestExe("console");
public AppTestExe Gui => GetAppTestExe(null);
public AppTestExe Worker => GetAppTestExe("worker");
private AppTestExe GetAppTestExe(string argPrefix)
{
return new AppTestExe(
Path.Combine(AppTestHelper.SolutionRoot, "NAPS2.App.Gtk", "bin", "Debug", "net6"),
"naps2",
argPrefix);
}
public override string ToString() => "Linux";
}

View File

@ -0,0 +1,18 @@
namespace NAPS2.App.Tests.Targets;
public class MacAppTestTarget : IAppTestTarget
{
public AppTestExe Console => GetAppTestExe("console");
public AppTestExe Gui => GetAppTestExe(null);
public AppTestExe Worker => GetAppTestExe("worker");
private AppTestExe GetAppTestExe(string argPrefix)
{
return new AppTestExe(
Path.Combine(AppTestHelper.SolutionRoot, "NAPS2.App.Mac", "bin", "Debug", "net7-macos10.15"),
Path.Combine("NAPS2.app", "Contents", "MacOS", "NAPS2"),
argPrefix);
}
public override string ToString() => "Mac";
}

View File

@ -0,0 +1,18 @@
namespace NAPS2.App.Tests.Targets;
public class WinNet462AppTestTarget : IAppTestTarget
{
public AppTestExe Console => GetAppTestExe("NAPS2.Console.exe");
public AppTestExe Gui => GetAppTestExe("NAPS2.exe");
public AppTestExe Worker => GetAppTestExe("NAPS2.Worker.exe");
private AppTestExe GetAppTestExe(string exeName)
{
return new AppTestExe(
Path.Combine(AppTestHelper.SolutionRoot, "NAPS2.App.WinForms", "bin", "Debug", "net462"),
exeName,
TestRootSubPath: "lib");
}
public override string ToString() => "Windows (net462)";
}

View File

@ -0,0 +1,27 @@
using System.Collections;
namespace NAPS2.App.Tests.Verification;
public class InstallDirTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
if (OperatingSystem.IsWindows() && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NAPS2_TEST_VERIFY")))
{
yield return new object[] { Environment.GetEnvironmentVariable("NAPS2_TEST_ROOT") };
}
else if (OperatingSystem.IsMacOS())
{
// No tests yet
}
else if (OperatingSystem.IsLinux())
{
// No tests yet
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -2,12 +2,11 @@ using Xunit;
namespace NAPS2.App.Tests.Verification;
// TODO: Consider a fact with multiple conditional attributes, e.g. verify-only, zip-only, requires-appium, win-only
public sealed class VerifyFactAttribute : FactAttribute
public sealed class VerifyTheoryAttribute : TheoryAttribute
{
private bool _allowDebug;
public VerifyFactAttribute()
public VerifyTheoryAttribute()
{
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NAPS2_TEST_VERIFY")))
{

View File

@ -4,28 +4,31 @@ namespace NAPS2.App.Tests.Verification;
public class VerifyTextFilesTests
{
[VerifyFact]
public void License()
[VerifyTheory]
[ClassData(typeof(InstallDirTestData))]
public void License(string installDir)
{
var path = Path.Combine(AppTestHelper.GetBaseDirectory(), "license.txt");
var path = Path.Combine(installDir, "license.txt");
Assert.True(File.Exists(path));
var text = File.ReadAllText(path);
Assert.Contains("This program is free software", text);
}
[VerifyFact]
public void Contributors()
[VerifyTheory]
[ClassData(typeof(InstallDirTestData))]
public void Contributors(string installDir)
{
var path = Path.Combine(AppTestHelper.GetBaseDirectory(), "contributors.txt");
var path = Path.Combine(installDir, "contributors.txt");
Assert.True(File.Exists(path));
var text = File.ReadAllText(path);
Assert.Contains("Primary NAPS2 developer", text);
}
[VerifyFact]
public void AppSettings()
[VerifyTheory]
[ClassData(typeof(InstallDirTestData))]
public void AppSettings(string installDir)
{
var path = Path.Combine(AppTestHelper.GetBaseDirectory(), "appsettings.xml");
var path = Path.Combine(installDir, "appsettings.xml");
Assert.True(File.Exists(path));
var text = File.ReadAllText(path);
Assert.Contains("<AppConfig>", text);

View File

@ -1,3 +1,4 @@
using NAPS2.App.Tests.Targets;
using NAPS2.Sdk.Tests;
using Xunit;
@ -5,10 +6,11 @@ namespace NAPS2.App.Tests;
public class WinFormsAppTests : ContextualTests
{
[Fact]
public void CreatesWindow()
[Theory]
[ClassData(typeof(AppTestData))]
public void CreatesWindow(IAppTestTarget target)
{
var process = AppTestHelper.StartGuiProcess("NAPS2.exe", FolderPath);
var process = AppTestHelper.StartGuiProcess(target.Gui, FolderPath);
try
{
AppTestHelper.WaitForVisibleWindow(process);

View File

@ -1,4 +1,5 @@
using GrpcDotNetNamedPipes;
using NAPS2.App.Tests.Targets;
using NAPS2.Remoting.Worker;
using NAPS2.Sdk.Tests;
using Xunit;
@ -7,10 +8,11 @@ namespace NAPS2.App.Tests;
public class WorkerAppTests : ContextualTests
{
[Fact]
public void CreatesPipeServer()
[Theory]
[ClassData(typeof(AppTestData))]
public void CreatesPipeServer(IAppTestTarget target)
{
var process = AppTestHelper.StartProcess("NAPS2.Worker.exe", FolderPath, Process.GetCurrentProcess().Id.ToString());
var process = AppTestHelper.StartProcess(target.Worker, FolderPath, Process.GetCurrentProcess().Id.ToString());
try
{
Assert.Equal("ready", process.StandardOutput.ReadLine());