Add regression tests for GB/C using various testroms

This commit is contained in:
YoshiRulz 2021-05-28 15:32:42 +10:00 committed by James Groom
parent 934a3ae266
commit 940dc69ae5
25 changed files with 2965 additions and 1 deletions

3
.gitignore vendored
View File

@ -31,7 +31,8 @@
/src/BizHawk.Common/VersionInfo.gen.cs
/src/BizHawk.Tests*/res/*_artifact
/src/BizHawk.Tests*/res/fw
/Build/*.vshost.*

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<Import Project="../MainSlnCommon.props" />
<PropertyGroup>
<OutputPath>$(ProjectDir)../../test_output</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" PrivateAssets="all" />
<ProjectReference Include="$(ProjectDir)../BizHawk.Tests.Testroms.GB/BizHawk.Tests.Testroms.GB.csproj" />
<EmbeddedResource Include="res/**/*" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using BizHawk.Common.CollectionExtensions;
using BizHawk.Common.IOExtensions;
using BizHawk.Common.StringExtensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static BizHawk.Tests.Testroms.GB.GBHelper;
namespace BizHawk.Tests.Testroms.GB.GambatteSuite
{
[TestClass]
public sealed partial class GambatteSuite
{
/// <remarks>there are 4664 * 3 cores = 13992 of these tests @_0</remarks>
[AttributeUsage(AttributeTargets.Method)]
private sealed class GambatteHexStrTestDataAttribute : Attribute, ITestDataSource
{
public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
{
if (!RomsArePresent) return new[] { new object?[] { GambatteHexStrTestCase.Dummy } };
return (_allCases ??= EnumerateAllCases()).HexStrCases
// .Where(static testCase => testCase.Setup.Variant is ConsoleVariant.DMG) // uncomment and modify to run a subset of the test cases...
.Where(static testCase => !TestUtils.ShouldIgnoreCase(SUITE_ID, testCase.DisplayName())) // ...or use the global blocklist in TestUtils
.OrderBy(static testCase => testCase.DisplayName())
.Select(static testCase => new object?[] { testCase });
}
public string GetDisplayName(MethodInfo methodInfo, object?[] data)
=> $"{methodInfo.Name}({((GambatteHexStrTestCase) data[0]!).DisplayName()})";
}
[AttributeUsage(AttributeTargets.Method)]
private sealed class GambatteRefImageTestDataAttribute : Attribute, ITestDataSource
{
public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
{
if (!RomsArePresent) return new[] { new object?[] { GambatteRefImageTestCase.Dummy } };
return (_allCases ??= EnumerateAllCases()).RefImageCases
// .Where(static testCase => testCase.Setup.Variant is ConsoleVariant.DMG) // uncomment and modify to run a subset of the test cases...
.Where(static testCase => !TestUtils.ShouldIgnoreCase(SUITE_ID, testCase.DisplayName())) // ...or use the global blocklist in TestUtils
.OrderBy(static testCase => testCase.DisplayName())
.Select(static testCase => new object?[] { testCase });
}
public string GetDisplayName(MethodInfo methodInfo, object?[] data)
=> $"{methodInfo.Name}({((GambatteRefImageTestCase) data[0]!).DisplayName()})";
}
private static readonly byte[,] GLYPHS = {
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000 },
{ 0b01111111, 0b00001000, 0b01111111, 0b01111111, 0b01000001, 0b01111111, 0b01111111, 0b01111111, 0b00111110, 0b01111111, 0b00001000, 0b01111110, 0b00111110, 0b01111110, 0b01111111, 0b01111111 },
{ 0b01000001, 0b00001000, 0b00000001, 0b00000001, 0b01000001, 0b01000000, 0b01000000, 0b00000001, 0b01000001, 0b01000001, 0b00100010, 0b01000001, 0b01000001, 0b01000001, 0b01000000, 0b01000000 },
{ 0b01000001, 0b00001000, 0b00000001, 0b00000001, 0b01000001, 0b01000000, 0b01000000, 0b00000010, 0b01000001, 0b01000001, 0b01000001, 0b01000001, 0b01000000, 0b01000001, 0b01000000, 0b01000000 },
{ 0b01000001, 0b00001000, 0b01111111, 0b00111111, 0b01111111, 0b01111110, 0b01111111, 0b00000100, 0b00111110, 0b01111111, 0b01111111, 0b01111110, 0b01000000, 0b01000001, 0b01111111, 0b01111111 },
{ 0b01000001, 0b00001000, 0b01000000, 0b00000001, 0b00000001, 0b00000001, 0b01000001, 0b00001000, 0b01000001, 0b00000001, 0b01000001, 0b01000001, 0b01000000, 0b01000001, 0b01000000, 0b01000000 },
{ 0b01000001, 0b00001000, 0b01000000, 0b00000001, 0b00000001, 0b00000001, 0b01000001, 0b00010000, 0b01000001, 0b00000001, 0b01000001, 0b01000001, 0b01000001, 0b01000001, 0b01000000, 0b01000000 },
{ 0b01111111, 0b00001000, 0b01111111, 0b01111111, 0b00000001, 0b01111110, 0b01111111, 0b00010000, 0b00111110, 0b01111111, 0b01000001, 0b01111110, 0b00111110, 0b01111110, 0b01111111, 0b01000000 },
};
private const string SUITE_ID = "GambatteSuite";
private const string SUITE_PREFIX = "res.Gambatte_testroms_artifact.";
private static (IReadOnlyList<GambatteRefImageTestCase> RefImageCases, IReadOnlyList<GambatteHexStrTestCase> HexStrCases)? _allCases = null;
private static readonly Regex HexStrFilenameRegex = new(@"_out[0-9A-F]+\.gbc?$");
private static readonly bool RomsArePresent = ReflectionCache.EmbeddedResourceList().Any(static s => s.StartsWith(SUITE_PREFIX));
[ClassCleanup]
public static void AfterAll()
=> TestUtils.WriteMetricsToDisk();
[ClassInitialize]
public static void BeforeAll(TestContext ctx)
=> TestUtils.PrepareDBAndOutput(SUITE_ID);
private static (IReadOnlyList<GambatteRefImageTestCase> RefImageCases, IReadOnlyList<GambatteHexStrTestCase> HexStrCases) EnumerateAllCases()
{
var variants = new[] { ("_cgb04c.png", ConsoleVariant.CGB_C), ("_dmg08.png", ConsoleVariant.DMG) };
static IReadOnlyList<(ConsoleVariant Variant, string ExpectValue)> ParseHexStrFilename(string filename)
{
List<(ConsoleVariant Variant, string Expect)> parsed = new();
string? lastSeenValue = null;
var endIndex = filename.LastIndexOf('.');
while (true)
{
var i = filename.LastIndexOf('_', endIndex - 1);
var seg = filename[(i + 1)..endIndex];
if (seg == "cgb04c") parsed.Add((ConsoleVariant.CGB_C, lastSeenValue!));
else if (seg == "dmg08") parsed.Add((ConsoleVariant.DMG, lastSeenValue!));
else if (seg.StartsWith("out")) lastSeenValue = seg.SubstringAfter("out");
else return parsed;
endIndex = i;
}
}
var allFilenames = ReflectionCache.EmbeddedResourceList(SUITE_PREFIX).ToList();
List<GambatteRefImageTestCase> refImageCases = new();
foreach (var filename in allFilenames.Where(static item => item.EndsWith(".png")).ToList())
{
var found = variants.FirstOrNull(kvp => filename.EndsWith(kvp.Item1));
if (found is null) continue;
var (suffix, variant) = found.Value;
var testName = filename.RemoveSuffix(suffix);
var romEmbedPath = $"{testName}.{(testName.StartsWith("dmgpalette_during_m3") ? "gb" : "gbc")}";
foreach (var setup in CoreSetup.ValidSetupsFor(variant))
{
refImageCases.Add(new(testName, setup, SUITE_PREFIX + romEmbedPath, SUITE_PREFIX + filename));
}
allFilenames.Remove(filename);
allFilenames.Remove(romEmbedPath);
}
var hexStrFilenames = allFilenames.Where(static s => HexStrFilenameRegex.IsMatch(s)).ToList();
List<GambatteHexStrTestCase> hexStrCases = new();
foreach (var hexStrFilename in hexStrFilenames)
{
var testName = hexStrFilename.SubstringBeforeLast('.');
foreach (var (variant, expectValue) in ParseHexStrFilename(hexStrFilename)) foreach (var setup in CoreSetup.ValidSetupsFor(variant))
{
hexStrCases.Add(new(testName, setup, SUITE_PREFIX + hexStrFilename, expectValue));
}
allFilenames.Remove(hexStrFilename);
}
// Console.WriteLine($"unused files:\n>>>\n{string.Join("\n", allFilenames.OrderBy(static s => s))}\n<<<");
return (refImageCases, hexStrCases);
}
[DataTestMethod]
[GambatteHexStrTestData]
public void RunGambatteHexStrTest(GambatteHexStrTestCase testCase)
{
static bool GlyphMatches(Bitmap b, int xOffset, byte v)
{
// `(x, 0)` is the top-left of an 8x8 square of pixels to read from `b`, which is compared against the glyph for the nybble `v`
bool GlyphRowMatches(int y)
{
byte rowAsByte = 0;
for (int x = xOffset, l = x + 8; x < l; x++)
{
rowAsByte <<= 1;
if ((b.GetPixel(x, y).ToArgb() & 0xFFFFFF) == 0) rowAsByte |= 1;
}
return rowAsByte == GLYPHS[y, v];
}
for (var y = 0; y < 8; y++) if (!GlyphRowMatches(y)) return false;
return true;
}
TestUtils.ShortCircuitMissingRom(RomsArePresent);
var caseStr = testCase.DisplayName();
TestUtils.ShortCircuitKnownFailure(caseStr, GambatteHexStrTestCase.KnownFailures, out var knownFail);
var actualUnnormalised = DummyFrontend.RunAndScreenshot(
InitGBCore(testCase.Setup, testCase.RomEmbedPath.SubstringBeforeLast('.'), ReflectionCache.EmbeddedResourceStream(testCase.RomEmbedPath).ReadAllBytes()),
static fe => fe.FrameAdvanceBy(11)).AsBitmap();
var glyphCount = testCase.ExpectedValue.Length;
var screenshotMatches = true;
var i = 0;
var xOffset = 0;
while (i < glyphCount)
{
if (!GlyphMatches(actualUnnormalised, xOffset, byte.Parse(testCase.ExpectedValue[i..(i + 1)], NumberStyles.HexNumber)))
{
screenshotMatches = false;
break;
}
i++;
xOffset += 8;
}
var state = TestUtils.SuccessState(screenshotMatches, knownFail);
if (!ImageUtils.SkipFileIO(state))
{
ImageUtils.SaveScreenshot(NormaliseGBScreenshot(actualUnnormalised, testCase.Setup), (SUITE_ID, caseStr));
Console.WriteLine($"should read: {testCase.ExpectedValue}");
}
switch (state)
{
case TestUtils.TestSuccessState.ExpectedFailure:
Assert.Inconclusive("expected failure, verified");
break;
case TestUtils.TestSuccessState.Failure:
Assert.Fail("screenshot contains incorrect value");
break;
case TestUtils.TestSuccessState.UnexpectedSuccess:
Assert.Fail("screenshot contains correct value unexpectedly (this is a good thing)");
break;
}
}
[DataTestMethod]
[GambatteRefImageTestData]
public void RunGambatteRefImageTest(GambatteRefImageTestCase testCase)
{
TestUtils.ShortCircuitMissingRom(RomsArePresent);
var caseStr = testCase.DisplayName();
TestUtils.ShortCircuitKnownFailure(caseStr, GambatteRefImageTestCase.KnownFailures, out var knownFail);
var actualUnnormalised = DummyFrontend.RunAndScreenshot(
InitGBCore(testCase.Setup, testCase.RomEmbedPath.SubstringBeforeLast('.'), ReflectionCache.EmbeddedResourceStream(testCase.RomEmbedPath).ReadAllBytes()),
static fe => fe.FrameAdvanceBy(14));
var state = GBScreenshotsEqual(
ReflectionCache.EmbeddedResourceStream(testCase.ExpectEmbedPath),
actualUnnormalised,
knownFail,
testCase.Setup,
(SUITE_ID, caseStr));
switch (state)
{
case TestUtils.TestSuccessState.ExpectedFailure:
Assert.Inconclusive("expected failure, verified");
break;
case TestUtils.TestSuccessState.Failure:
Assert.Fail("expected and actual screenshots differ");
break;
case TestUtils.TestSuccessState.UnexpectedSuccess:
Assert.Fail("expected and actual screenshots matched unexpectedly (this is a good thing)");
break;
}
}
}
}

View File

@ -0,0 +1,3 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: TestDataSourceDiscovery(TestDataSourceDiscoveryOption.DuringExecution)]

View File

@ -0,0 +1,15 @@
See the readme in the main project, `../BizHawk.Tests.Testroms.GB`.
On Linux, run `/res/download_from_ci.sh` to automatically download and extract the CI artifacts containing the necessary testroms.
On Windows, run the same script in WSL, or do it manually (because Yoshi can't be bothered porting the script to PowerShell).
For this project, the expected directory structure is:
```
res
└─ Gambatte-testroms_artifact
```
Note that firmware does not need to be copied here. They are taken from `../BizHawk.Tests.Testroms.GB/res/fw` if present.
> This test suite is huge and takes a **really long time** to run. Like several hours.
Summary of `BIZHAWKTEST_RUN_KNOWN_FAILURES=1 ./run_tests_release.sh` should read 13681 passed / 1304 skipped / 0 failed.

View File

@ -0,0 +1,4 @@
#!/bin/sh
set -e
cd "$(dirname "$(realpath "$0")")"
../../BizHawk.Tests.Testroms.GB/.download_from_ci.sh Gambatte-testroms

View File

@ -0,0 +1,2 @@
#!/bin/sh
cd "$(dirname "$(realpath "$0")")" && ../BizHawk.Tests.Testroms.GB/.run_tests_with_configuration.sh "Debug" "$@"

View File

@ -0,0 +1,2 @@
#!/bin/sh
cd "$(dirname "$(realpath "$0")")" && ../BizHawk.Tests.Testroms.GB/.run_tests_with_configuration.sh "Release" "$@"

View File

@ -0,0 +1,23 @@
#!/bin/sh
set -e
for j in "$@"; do
if [ -e "${j}_artifact" ]; then
printf "Using existing copy of %s\n" "$j"
else
curl -L -o "$j.zip" "https://gitlab.com/tasbot/libre-roms-ci/-/jobs/artifacts/master/download?job=$j"
unzip "$j.zip" >/dev/null
find "${j}_artifact" -type d -exec chmod 755 "{}" \;
find "${j}_artifact" -type f -exec chmod 644 "{}" \;
rm "$j.zip"
printf "Downloaded and extracted %s CI artifact\n" "$j"
fi
done
exit 0
# TODO finish this and put it in a separate script
nixVersion="$(nix --version 2>&1)"
if [ $? -eq 0 ]; then
for a in blargg-gb-tests; do
printf "(TODO: nix-build %s)\n" "$a"
done
fi

View File

@ -0,0 +1,8 @@
#!/bin/sh
set -e
root="$(realpath "$PWD/../..")"
config="$1"
shift
res/download_from_ci.sh
export LD_LIBRARY_PATH="$root/output/dll:$LD_LIBRARY_PATH"
dotnet test -a "$root/test_output" -c "$config" -l "junit;LogFilePath=$root/test_output/{assembly}.coverage.xml;MethodFormat=Class;FailureBodyFormat=Verbose" -l "console;verbosity=detailed" "$@"

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<Import Project="../MainSlnCommon.props" />
<PropertyGroup>
<OutputPath>$(ProjectDir)../../test_output</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(BIZHAWKTEST_RUN_KNOWN_FAILURES)' == '' ">
<DefineConstants>$(DefineConstants);SKIP_KNOWN_FAILURES</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(BIZHAWKTEST_SAVE_IMAGES)' == '' OR '$(BIZHAWKTEST_SAVE_IMAGES)' == 'failures' ">
<DefineConstants>$(DefineConstants);SAVE_IMAGES_ON_FAIL</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(BIZHAWKTEST_SAVE_IMAGES)' == 'all' ">
<DefineConstants>$(DefineConstants);SAVE_IMAGES_ON_FAIL;SAVE_IMAGES_ON_PASS</DefineConstants>
</PropertyGroup>
<!-- BIZHAWKTEST_SAVE_IMAGES=none => no extra defines -->
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" PrivateAssets="all" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.98" />
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="8.4.0" />
<ProjectReference Include="$(ProjectDir)../BizHawk.Client.Common/BizHawk.Client.Common.csproj" />
<EmbeddedResource Include="res/**/*" />
<Content Include="$(ProjectDir)../../Assets/gamedb/**/*" LinkBase="gamedb" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using BizHawk.Bizware.BizwareGL;
using BizHawk.Client.Common;
using BizHawk.Common.IOExtensions;
using BizHawk.Emulation.Common;
namespace BizHawk.Tests.Testroms.GB
{
public sealed class DummyFrontend : IDisposable
{
public sealed class EmbeddedFirmwareProvider : ICoreFileProvider
{
private static string FailMsg(string embedPath, string? msg)
=> $"failed to open required resource at {embedPath}, is it present in $(ProjectDir)/res?{(msg is not null ? " core says: " + msg : string.Empty)}";
public readonly IDictionary<FirmwareID, string> EmbedPathMap;
public EmbeddedFirmwareProvider(IDictionary<FirmwareID, string>? embedPathMap = null)
=> EmbedPathMap = embedPathMap ?? new Dictionary<FirmwareID, string>();
/// <returns><see langword="true"/> iff succeeded</returns>
public bool AddIfExists(FirmwareID id, string embedPath)
{
var exists = ReflectionCache.EmbeddedResourceList().Contains(embedPath);
if (exists) EmbedPathMap[id] = embedPath;
return exists;
}
public string DllPath()
=> throw new NotImplementedException();
private (string EmbedPath, byte[]? FW) GetFirmwareInner(FirmwareID id)
{
var embedPath = EmbedPathMap[id];
Stream embeddedResourceStream;
try
{
embeddedResourceStream = ReflectionCache.EmbeddedResourceStream(embedPath);
}
catch (Exception)
{
return (embedPath, null);
}
var fw = embeddedResourceStream.ReadAllBytes();
embeddedResourceStream.Dispose();
return (embedPath, fw);
}
public byte[]? GetFirmware(FirmwareID id, string? msg = null)
{
var (embedPath, fw) = GetFirmwareInner(id);
if (fw is null) Console.WriteLine(FailMsg(embedPath, msg));
return fw;
}
public byte[] GetFirmwareOrThrow(FirmwareID id, string? msg = null)
{
var (embedPath, fw) = GetFirmwareInner(id);
if (fw is null) throw new Exception(FailMsg(embedPath, msg));
return fw;
}
public (byte[] FW, GameInfo Game) GetFirmwareWithGameInfoOrThrow(FirmwareID id, string? msg = null)
=> throw new NotImplementedException(); // only used by PCEHawk
public string GetRetroSaveRAMDirectory(IGameInfo game)
=> throw new NotImplementedException();
public string GetRetroSystemPath(IGameInfo game)
=> throw new NotImplementedException();
}
private sealed class FakeGraphicsControl : IGraphicsControl
{
private readonly IGL_GdiPlus _gdi;
private readonly Func<(int, int)> _getVirtualSize;
public Rectangle ClientRectangle
{
get
{
var (w, h) = _getVirtualSize();
return new(0, 0, w, h);
}
}
public RenderTargetWrapper? RenderTargetWrapper { get; set; }
public FakeGraphicsControl(IGL_GdiPlus glImpl, Func<(int Width, int Height)> getVirtualSize)
{
_gdi = glImpl;
_getVirtualSize = getVirtualSize;
}
public void Begin()
{
_gdi.BeginControl(this);
RenderTargetWrapper!.CreateGraphics();
}
public Graphics CreateGraphics()
{
var (w, h) = _getVirtualSize();
return Graphics.FromImage(new Bitmap(w, h));
}
public void End()
=> _gdi.EndControl(this);
public void SetVsync(bool state) {}
public void SwapBuffers()
{
_gdi.SwapControl(this);
if (RenderTargetWrapper!.MyBufferedGraphics is null) return;
RenderTargetWrapper.CreateGraphics();
}
public void Dispose() {}
}
public sealed class SimpleGDIPDisplayManager : DisplayManagerBase
{
private readonly FakeGraphicsControl _gc;
private SimpleGDIPDisplayManager(Config config, IEmulator emuCore, IGL_GdiPlus glImpl)
: base(config, emuCore, inputManager: null, movieSession: null, EDispMethod.GdiPlus, glImpl, new GDIPlusGuiRenderer(glImpl))
{
_gc = (FakeGraphicsControl) glImpl.Internal_CreateGraphicsControl();
Blank();
}
public SimpleGDIPDisplayManager(Config config, IEmulator emuCore, Func<(int Width, int Height)> getVirtualSize)
: this(config, emuCore, new IGL_GdiPlus(self => new FakeGraphicsControl(self, getVirtualSize))) {}
protected override void ActivateGLContext()
=> _gc.Begin();
protected override void SwapBuffersOfGraphicsControl()
=> _gc.SwapBuffers();
}
private static int _totalFrames = 0;
private static readonly object _totalFramesMutex = new();
public static int TotalFrames
{
get
{
lock (_totalFramesMutex) return _totalFrames;
}
}
/// <summary>
/// set-up firmwares on <paramref name="efp"/>, optionally setting <paramref name="config"/>, then
/// initialise and return a core instance (<paramref name="coreComm"/> is provided),
/// and optionally specify a frame number to seek to (e.g. to skip BIOS screens)
/// </summary>
public delegate (IEmulator NewCore, int BiosWaitDuration) ClassInitCallbackDelegate(
EmbeddedFirmwareProvider efp,
Config config,
CoreComm coreComm);
public static Bitmap RunAndScreenshot(ClassInitCallbackDelegate init, Action<DummyFrontend> run)
{
using DummyFrontend fe = new(init);
run(fe);
return fe.Screenshot();
}
private readonly Config _config = new();
private readonly SimpleController _controller;
private readonly IVideoProvider _coreAsVP;
private readonly SimpleGDIPDisplayManager _dispMan;
public readonly IEmulator Core;
public readonly IDebuggable? CoreAsDebuggable;
public readonly IMemoryDomains? CoreAsMemDomains;
public int FrameCount => Core.Frame;
/// <seealso cref="ClassInitCallbackDelegate"/>
public DummyFrontend(ClassInitCallbackDelegate init)
{
EmbeddedFirmwareProvider efp = new();
var (core, biosWaitDuration) = init(
efp,
_config,
new(Console.WriteLine, Console.WriteLine, efp, CoreComm.CorePreferencesFlags.None));
Core = core;
_controller = new(Core.ControllerDefinition);
while (Core.Frame < biosWaitDuration) Core.FrameAdvance(_controller, render: false, renderSound: false);
CoreAsDebuggable = Core.CanDebug() ? Core.AsDebuggable() : null;
CoreAsMemDomains = Core.HasMemoryDomains() ? Core.AsMemoryDomains() : null;
_coreAsVP = core.AsVideoProvider();
_dispMan = new(_config, core, () => (_coreAsVP!.VirtualWidth, _coreAsVP.VirtualHeight));
}
public void Dispose()
{
_dispMan.Dispose();
lock (_totalFramesMutex) _totalFrames += FrameCount;
}
public void FrameAdvance()
=> Core.FrameAdvance(_controller, render: false, renderSound: false);
public void FrameAdvanceBy(int numFrames)
=> FrameAdvanceTo(FrameCount + numFrames);
/// <returns>last return of <paramref name="pred"/> (will be <see langword="false"/> iff timed out)</returns>
/// <remarks><paramref name="timeoutAtFrame"/> is NOT relative to current frame count</remarks>
public bool FrameAdvanceUntil(Func<bool> pred, int timeoutAtFrame = 500)
{
while (!pred() && FrameCount < timeoutAtFrame) FrameAdvance();
return FrameCount < timeoutAtFrame;
}
public void FrameAdvanceTo(int frame)
{
while (FrameCount < frame) FrameAdvance();
}
public Bitmap Screenshot()
=> _dispMan.RenderVideoProvider(_coreAsVP).ToSysdrawingBitmap();
public void SetButton(string buttonName)
=> _controller[buttonName] = true;
}
}

View File

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BizHawk.Common.IOExtensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static BizHawk.Tests.Testroms.GB.GBHelper;
namespace BizHawk.Tests.Testroms.GB
{
[TestClass]
public sealed class AcidTestroms
{
public readonly struct AcidTestCase
{
public string ExpectEmbedPath => TestName switch
{
"cgb-acid-hell" => "res.cgb_acid_hell_artifact.reference.png",
"cgb-acid2" => "res.cgb_acid2_artifact.reference.png",
"dmg-acid2" => $"res.dmg_acid2_artifact.reference-{(Setup.Variant.IsColour() ? "cgb" : "dmg")}.png",
_ => throw new Exception()
};
public readonly string RomEmbedPath => TestName switch
{
"cgb-acid-hell" => "res.cgb_acid_hell_artifact.cgb-acid-hell.gbc",
"cgb-acid2" => "res.cgb_acid2_artifact.cgb-acid2.gbc",
"dmg-acid2" => "res.dmg_acid2_artifact.dmg-acid2.gb",
_ => throw new Exception()
};
public readonly CoreSetup Setup;
public readonly string TestName;
public AcidTestCase(string testName, CoreSetup setup)
{
Setup = setup;
TestName = testName;
}
public readonly string DisplayName()
=> $"{TestName} on {Setup}";
}
[AttributeUsage(AttributeTargets.Method)]
private sealed class AcidTestDataAttribute : Attribute, ITestDataSource
{
public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
{
List<AcidTestCase> testCases = new();
foreach (var setup in CoreSetup.ValidSetupsFor(ConsoleVariant.CGB_C))
{
testCases.Add(new("cgb-acid-hell", setup));
testCases.Add(new("cgb-acid2", setup));
testCases.Add(new("dmg-acid2", setup));
}
foreach (var setup in CoreSetup.ValidSetupsFor(ConsoleVariant.DMG))
{
testCases.Add(new("dmg-acid2", setup));
}
// testCases.RemoveAll(static testCase => testCase.Setup.Variant is not ConsoleVariant.DMG); // uncomment and modify to run a subset of the test cases...
testCases.RemoveAll(static testCase => TestUtils.ShouldIgnoreCase(SUITE_ID, testCase.DisplayName())); // ...or use the global blocklist in TestUtils
return testCases.OrderBy(static testCase => testCase.DisplayName())
.Select(static testCase => new object?[] { testCase });
}
public string GetDisplayName(MethodInfo methodInfo, object?[] data)
=> $"{methodInfo.Name}({((AcidTestCase) data[0]!).DisplayName()})";
}
private const string SUITE_ID = "AcidTestroms";
private static readonly IReadOnlyList<string> FilteredEmbedPaths = ReflectionCache.EmbeddedResourceList().Where(static s => s.Contains("acid")).ToList();
private static readonly IReadOnlyCollection<string> KnownFailures = new[]
{
"cgb-acid-hell on CGB_C in Gambatte",
"cgb-acid-hell on CGB_C in Gambatte (no BIOS)",
"dmg-acid2 on CGB_C in Gambatte",
"dmg-acid2 on CGB_C in Gambatte (no BIOS)",
};
[ClassCleanup]
public static void AfterAll()
=> TestUtils.WriteMetricsToDisk();
[ClassInitialize]
public static void BeforeAll(TestContext ctx)
=> TestUtils.PrepareDBAndOutput(SUITE_ID);
[AcidTestData]
[DataTestMethod]
public void RunAcidTest(AcidTestCase testCase)
{
TestUtils.ShortCircuitMissingRom(isPresent: FilteredEmbedPaths.Contains(testCase.RomEmbedPath));
var caseStr = testCase.DisplayName();
TestUtils.ShortCircuitKnownFailure(caseStr, KnownFailures, out var knownFail);
var actualUnnormalised = DummyFrontend.RunAndScreenshot(
InitGBCore(testCase.Setup, $"{testCase.TestName}.gbc", ReflectionCache.EmbeddedResourceStream(testCase.RomEmbedPath).ReadAllBytes()),
static fe => fe.FrameAdvanceBy(15));
var state = GBScreenshotsEqual(
ReflectionCache.EmbeddedResourceStream(testCase.ExpectEmbedPath),
actualUnnormalised,
knownFail,
testCase.Setup,
(SUITE_ID, caseStr),
MattCurriePaletteMap);
switch (state)
{
case TestUtils.TestSuccessState.ExpectedFailure:
Assert.Inconclusive("expected failure, verified");
break;
case TestUtils.TestSuccessState.Failure:
Assert.Fail("expected and actual screenshots differ");
break;
case TestUtils.TestSuccessState.UnexpectedSuccess:
Assert.Fail("expected and actual screenshots matched unexpectedly (this is a good thing)");
break;
}
}
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BizHawk.Common.IOExtensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static BizHawk.Tests.Testroms.GB.GBHelper;
namespace BizHawk.Tests.Testroms.GB
{
[TestClass]
public sealed class BullyGB
{
[AttributeUsage(AttributeTargets.Method)]
private sealed class BullyTestData : Attribute, ITestDataSource
{
public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
{
var testCases = new[] { ConsoleVariant.CGB_C, ConsoleVariant.DMG }.SelectMany(CoreSetup.ValidSetupsFor).ToList();
// testCases.RemoveAll(static setup => setup.Variant is not ConsoleVariant.DMG); // uncomment and modify to run a subset of the test cases...
testCases.RemoveAll(static setup => TestUtils.ShouldIgnoreCase(SUITE_ID, DisplayNameFor(setup))); // ...or use the global blocklist in TestUtils
return testCases.OrderBy(static setup => setup.ToString())
.Select(static setup => new object?[] { setup });
}
public string GetDisplayName(MethodInfo methodInfo, object?[] data)
=> $"{methodInfo.Name}({(CoreSetup) data[0]!})";
}
private const string ROM_EMBED_PATH = "res.BullyGB_artifact.bully.gb";
private const string SUITE_ID = "BullyGB";
private static readonly IReadOnlyCollection<string> KnownFailures = new[]
{
"BullyGB on CGB_C in GBHawk",
};
private static readonly bool RomIsPresent = ReflectionCache.EmbeddedResourceList().Contains(ROM_EMBED_PATH);
[ClassCleanup]
public static void AfterAll()
=> TestUtils.WriteMetricsToDisk();
[ClassInitialize]
public static void BeforeAll(TestContext ctx)
=> TestUtils.PrepareDBAndOutput(SUITE_ID);
private static string DisplayNameFor(CoreSetup setup)
=> $"BullyGB on {setup}";
[BullyTestData]
[DataTestMethod]
public void RunBullyTest(CoreSetup setup)
{
TestUtils.ShortCircuitMissingRom(RomIsPresent);
var caseStr = DisplayNameFor(setup);
TestUtils.ShortCircuitKnownFailure(caseStr, KnownFailures, out var knownFail);
var actualUnnormalised = DummyFrontend.RunAndScreenshot(
InitGBCore(setup, "bully.gbc", ReflectionCache.EmbeddedResourceStream(ROM_EMBED_PATH).ReadAllBytes()),
static fe => fe.FrameAdvanceBy(18));
var state = GBScreenshotsEqual(
ReflectionCache.EmbeddedResourceStream($"res.BullyGB_artifact.expected_{(setup.Variant.IsColour() ? "cgb" : "dmg")}.png"),
actualUnnormalised,
knownFail,
setup,
(SUITE_ID, caseStr));
switch (state)
{
case TestUtils.TestSuccessState.ExpectedFailure:
Assert.Inconclusive("expected failure, verified");
break;
case TestUtils.TestSuccessState.Failure:
Assert.Fail("expected and actual screenshots differ");
break;
case TestUtils.TestSuccessState.UnexpectedSuccess:
Assert.Fail("expected and actual screenshots matched unexpectedly (this is a good thing)");
break;
}
}
}
}

View File

@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores;
using BizHawk.Emulation.Cores.Nintendo.Gameboy;
using BizHawk.Emulation.Cores.Nintendo.GBHawk;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static BizHawk.Emulation.Cores.Nintendo.Gameboy.Gameboy;
using static BizHawk.Emulation.Cores.Nintendo.GBHawk.GBHawk;
namespace BizHawk.Tests.Testroms.GB
{
public static class GBHelper
{
public enum ConsoleVariant { CGB_C, CGB_D, DMG, DMG_B }
public readonly struct CoreSetup
{
public static IReadOnlyCollection<CoreSetup> ValidSetupsFor(ConsoleVariant variant)
=> new CoreSetup[] { new(CoreNames.Gambatte, variant), new(CoreNames.Gambatte, variant, useBios: false), new(CoreNames.GbHawk, variant) };
public readonly string CoreName;
public readonly bool UseBIOS;
public readonly ConsoleVariant Variant;
public CoreSetup(string coreName, ConsoleVariant variant, bool useBios = true)
{
CoreName = coreName;
UseBIOS = useBios;
Variant = variant;
}
public override readonly string ToString()
=> $"{Variant} in {CoreName}{(UseBIOS ? string.Empty : " (no BIOS)")}";
}
private static readonly GambatteSettings GambatteSettings = new() { CGBColors = GBColors.ColorType.vivid };
private static readonly GambatteSyncSettings GambatteSyncSettings_GB_NOBIOS = new() { ConsoleMode = GambatteSyncSettings.ConsoleModeType.GB, FrameLength = GambatteSyncSettings.FrameLengthType.EqualLengthFrames };
private static readonly GambatteSyncSettings GambatteSyncSettings_GB_USEBIOS = new() { ConsoleMode = GambatteSyncSettings.ConsoleModeType.GB, EnableBIOS = true, FrameLength = GambatteSyncSettings.FrameLengthType.EqualLengthFrames };
private static readonly GambatteSyncSettings GambatteSyncSettings_GBC_NOBIOS = new() { ConsoleMode = GambatteSyncSettings.ConsoleModeType.GBC, FrameLength = GambatteSyncSettings.FrameLengthType.EqualLengthFrames };
private static readonly GambatteSyncSettings GambatteSyncSettings_GBC_USEBIOS = new() { ConsoleMode = GambatteSyncSettings.ConsoleModeType.GBC, EnableBIOS = true, FrameLength = GambatteSyncSettings.FrameLengthType.EqualLengthFrames };
private static readonly GBSyncSettings GBHawkSyncSettings_GB = new() { ConsoleMode = GBSyncSettings.ConsoleModeType.GB };
private static readonly GBSyncSettings GBHawkSyncSettings_GBC = new() { ConsoleMode = GBSyncSettings.ConsoleModeType.GBC };
public static readonly IReadOnlyDictionary<int, int> MattCurriePaletteMap = new Dictionary<int, int>
{
[0x0F3EAA] = 0x0000FF,
[0x137213] = 0x009C00,
[0x187890] = 0x0063C6,
[0x695423] = 0x737300,
[0x7BC8D5] = 0x6BBDFF,
[0x7F3848] = 0x943939,
[0x83C656] = 0x7BFF31,
[0x9D7E34] = 0xADAD00,
[0xE18096] = 0xFF8484,
[0xE8BA4D] = 0xFFFF00,
[0xF8F8F8] = 0xFFFFFF,
};
public static readonly IReadOnlyDictionary<int, int> UnVividGBCPaletteMap = new Dictionary<int, int>
{
[0x0063C5] = 0x0063C6,
[0x00CE00] = 0x199619,
[0x089C84] = 0x21926C,
[0x424242] = 0x404040,
[0x52AD52] = 0x5B925B,
[0x943A3A] = 0x943939,
[0xA5A5A5] = 0xA0A0A0,
[0xAD52AD] = 0x9D669D,
[0xFFFFFF] = 0xF8F8F8,
};
public static readonly IReadOnlyDictionary<int, int> UnVividGBPaletteMap = new Dictionary<int, int>
{
[0x525252] = 0x555555,
[0xADADAD] = 0xAAAAAA,
};
private static bool AddEmbeddedGBBIOS(this DummyFrontend.EmbeddedFirmwareProvider efp, ConsoleVariant variant)
=> variant.IsColour()
? efp.AddIfExists(new("GBC", "World"), false ? "res.fw.GBC__World__AGB.bin" : "res.fw.GBC__World__CGB.bin")
: efp.AddIfExists(new("GB", "World"), "res.fw.GB__World__DMG.bin");
public static TestUtils.TestSuccessState GBScreenshotsEqual(
Stream expectFile,
Image? actualUnnormalised,
bool expectingNotEqual,
CoreSetup setup,
(string Suite, string Case) id,
IReadOnlyDictionary<int, int>? extraPaletteMap = null)
{
if (actualUnnormalised is null)
{
Assert.Fail("actual screenshot was null");
return TestUtils.TestSuccessState.Failure; // never hit
}
var actual = NormaliseGBScreenshot(actualUnnormalised, setup);
// ImageUtils.PrintPalette(Image.FromStream(expectFile), "expected image", actual, "actual image (after normalisation, before extra map)");
return ImageUtils.ScreenshotsEqualMagickDotNET(
expectFile,
extraPaletteMap is null ? actual : ImageUtils.PaletteSwap(actual, extraPaletteMap),
expectingNotEqual,
id);
}
public static GambatteSyncSettings GetGambatteSyncSettings(ConsoleVariant variant, bool biosAvailable)
=> biosAvailable
? variant.IsColour()
? GambatteSyncSettings_GBC_USEBIOS
: GambatteSyncSettings_GB_USEBIOS
: variant.IsColour()
? GambatteSyncSettings_GBC_NOBIOS
: GambatteSyncSettings_GB_NOBIOS;
public static GBSyncSettings GetGBHawkSyncSettings(ConsoleVariant variant)
=> variant.IsColour()
? GBHawkSyncSettings_GBC
: GBHawkSyncSettings_GB;
public static DummyFrontend.ClassInitCallbackDelegate InitGBCore(CoreSetup setup, string romFilename, byte[] rom)
=> (efp, _, coreComm) =>
{
if (setup.UseBIOS && !efp.AddEmbeddedGBBIOS(setup.Variant)) Assert.Inconclusive("BIOS not provided");
var game = Database.GetGameInfo(rom, romFilename);
IEmulator newCore = setup.CoreName switch
{
CoreNames.Gambatte => new Gameboy(coreComm, game, rom, GambatteSettings, GetGambatteSyncSettings(setup.Variant, setup.UseBIOS), deterministic: true),
CoreNames.GbHawk => new GBHawk(coreComm, game, rom, new(), GetGBHawkSyncSettings(setup.Variant)),
_ => throw new Exception()
};
var biosWaitDuration = setup.UseBIOS
? setup.Variant.IsColour()
? 186
: 334
: 0;
return (newCore, biosWaitDuration);
};
public static bool IsColour(this ConsoleVariant variant)
=> variant is ConsoleVariant.CGB_C or ConsoleVariant.CGB_D;
/// <summary>converts Gambatte's GBC palette to GBHawk's; GB palette is the same</summary>
public static Image NormaliseGBScreenshot(Image img, CoreSetup setup)
=> setup.CoreName is CoreNames.Gambatte
? ImageUtils.PaletteSwap(img, setup.Variant.IsColour() ? UnVividGBCPaletteMap : UnVividGBPaletteMap)
: img;
}
}

View File

@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BizHawk.Common.IOExtensions;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores;
using BizHawk.Emulation.Cores.Nintendo.Gameboy;
using BizHawk.Emulation.Cores.Nintendo.GBHawk;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static BizHawk.Tests.Testroms.GB.GBHelper;
namespace BizHawk.Tests.Testroms.GB
{
[TestClass]
public sealed class MealybugTearoomTests
{
public readonly struct MealybugTestCase
{
public static readonly MealybugTestCase Dummy = new("missing_files", new(CoreNames.Gambatte, ConsoleVariant.DMG), string.Empty, string.Empty);
public static readonly IReadOnlyCollection<string> KnownFailures = new[]
{
"m3_bgp_change on CGB_C in Gambatte", // Gambatte's GBC emulation matches CGB D variant
"m3_bgp_change on CGB_C in Gambatte (no BIOS)", // Gambatte's GBC emulation matches CGB D variant
"m3_bgp_change on CGB_C in GBHawk",
"m3_bgp_change on CGB_D in GBHawk",
"m3_bgp_change on DMG in Gambatte",
"m3_bgp_change on DMG in Gambatte (no BIOS)",
"m3_bgp_change on DMG in GBHawk",
"m3_bgp_change_sprites on CGB_C in Gambatte", // Gambatte's GBC emulation matches CGB D variant
"m3_bgp_change_sprites on CGB_C in Gambatte (no BIOS)", // Gambatte's GBC emulation matches CGB D variant
"m3_bgp_change_sprites on CGB_C in GBHawk",
"m3_bgp_change_sprites on CGB_D in GBHawk",
"m3_bgp_change_sprites on DMG in Gambatte",
"m3_bgp_change_sprites on DMG in Gambatte (no BIOS)",
"m3_bgp_change_sprites on DMG in GBHawk",
"m3_lcdc_bg_en_change on DMG in Gambatte",
"m3_lcdc_bg_en_change on DMG in Gambatte (no BIOS)",
"m3_lcdc_bg_en_change on DMG in GBHawk",
"m3_lcdc_bg_en_change on DMG_B in Gambatte",
"m3_lcdc_bg_en_change on DMG_B in Gambatte (no BIOS)",
"m3_lcdc_bg_en_change on DMG_B in GBHawk",
"m3_lcdc_bg_map_change on CGB_C in GBHawk",
"m3_lcdc_bg_map_change on DMG in GBHawk",
"m3_lcdc_bg_map_change2 on CGB_C in GBHawk",
"m3_lcdc_obj_en_change on DMG in Gambatte",
"m3_lcdc_obj_en_change on DMG in Gambatte (no BIOS)",
"m3_lcdc_obj_en_change on DMG in GBHawk",
"m3_lcdc_obj_en_change_variant on CGB_C in Gambatte", // Gambatte's GBC emulation matches CGB D variant
"m3_lcdc_obj_en_change_variant on CGB_C in Gambatte (no BIOS)", // Gambatte's GBC emulation matches CGB D variant
"m3_lcdc_obj_en_change_variant on CGB_C in GBHawk",
"m3_lcdc_obj_en_change_variant on CGB_D in GBHawk",
"m3_lcdc_obj_en_change_variant on DMG in Gambatte",
"m3_lcdc_obj_en_change_variant on DMG in Gambatte (no BIOS)",
"m3_lcdc_obj_en_change_variant on DMG in GBHawk",
"m3_lcdc_obj_size_change on CGB_C in GBHawk",
"m3_lcdc_obj_size_change on DMG in Gambatte",
"m3_lcdc_obj_size_change on DMG in Gambatte (no BIOS)",
"m3_lcdc_obj_size_change on DMG in GBHawk",
"m3_lcdc_obj_size_change_scx on CGB_C in GBHawk",
"m3_lcdc_obj_size_change_scx on DMG in Gambatte",
"m3_lcdc_obj_size_change_scx on DMG in Gambatte (no BIOS)",
"m3_lcdc_obj_size_change_scx on DMG in GBHawk",
"m3_lcdc_tile_sel_change on CGB_C in Gambatte",
"m3_lcdc_tile_sel_change on CGB_C in Gambatte (no BIOS)",
"m3_lcdc_tile_sel_change on CGB_C in GBHawk",
"m3_lcdc_tile_sel_change on DMG in GBHawk",
"m3_lcdc_tile_sel_change2 on CGB_C in Gambatte",
"m3_lcdc_tile_sel_change2 on CGB_C in Gambatte (no BIOS)",
"m3_lcdc_tile_sel_change2 on CGB_C in GBHawk",
"m3_lcdc_tile_sel_win_change on CGB_C in Gambatte",
"m3_lcdc_tile_sel_win_change on CGB_C in Gambatte (no BIOS)",
"m3_lcdc_tile_sel_win_change on CGB_C in GBHawk",
"m3_lcdc_tile_sel_win_change on DMG in GBHawk",
"m3_lcdc_tile_sel_win_change2 on CGB_C in Gambatte",
"m3_lcdc_tile_sel_win_change2 on CGB_C in Gambatte (no BIOS)",
"m3_lcdc_tile_sel_win_change2 on CGB_C in GBHawk",
"m3_lcdc_win_en_change_multiple on CGB_C in GBHawk",
"m3_lcdc_win_en_change_multiple on DMG in GBHawk",
"m3_lcdc_win_en_change_multiple_wx on DMG in Gambatte",
"m3_lcdc_win_en_change_multiple_wx on DMG in Gambatte (no BIOS)",
"m3_lcdc_win_en_change_multiple_wx on DMG in GBHawk",
"m3_lcdc_win_en_change_multiple_wx on DMG_B in Gambatte",
"m3_lcdc_win_en_change_multiple_wx on DMG_B in Gambatte (no BIOS)",
"m3_lcdc_win_en_change_multiple_wx on DMG_B in GBHawk",
"m3_lcdc_win_map_change on CGB_C in GBHawk",
"m3_lcdc_win_map_change on DMG in GBHawk",
"m3_lcdc_win_map_change2 on CGB_C in GBHawk",
"m3_obp0_change on CGB_C in Gambatte", // Gambatte's GBC emulation matches CGB D variant
"m3_obp0_change on CGB_C in Gambatte (no BIOS)", // Gambatte's GBC emulation matches CGB D variant
"m3_obp0_change on CGB_C in GBHawk",
"m3_obp0_change on CGB_D in GBHawk",
"m3_obp0_change on DMG in GBHawk",
"m3_scx_high_5_bits on CGB_C in GBHawk",
"m3_scx_high_5_bits on DMG in GBHawk",
"m3_scx_high_5_bits_change2 on CGB_C in GBHawk",
"m3_scy_change on CGB_C in GBHawk",
"m3_scy_change on CGB_D in Gambatte", // Gambatte's GBC emulation matches CGB C variant
"m3_scy_change on CGB_D in Gambatte (no BIOS)", // Gambatte's GBC emulation matches CGB C variant
"m3_scy_change on CGB_D in GBHawk",
"m3_scy_change on DMG in GBHawk",
"m3_scy_change2 on CGB_C in GBHawk",
"m3_window_timing on CGB_C in Gambatte", // Gambatte's GBC emulation matches CGB D variant
"m3_window_timing on CGB_C in Gambatte (no BIOS)", // Gambatte's GBC emulation matches CGB D variant
"m3_window_timing on CGB_C in GBHawk",
"m3_window_timing on CGB_D in GBHawk",
"m3_window_timing on DMG in GBHawk",
"m3_window_timing_wx_0 on CGB_C in Gambatte",
"m3_window_timing_wx_0 on CGB_C in Gambatte (no BIOS)",
"m3_window_timing_wx_0 on CGB_C in GBHawk",
"m3_window_timing_wx_0 on CGB_D in Gambatte",
"m3_window_timing_wx_0 on CGB_D in Gambatte (no BIOS)",
"m3_window_timing_wx_0 on CGB_D in GBHawk",
"m3_window_timing_wx_0 on DMG in Gambatte",
"m3_window_timing_wx_0 on DMG in Gambatte (no BIOS)",
"m3_window_timing_wx_0 on DMG in GBHawk",
"m3_wx_4_change on DMG in Gambatte",
"m3_wx_4_change on DMG in Gambatte (no BIOS)",
"m3_wx_4_change on DMG in GBHawk",
"m3_wx_4_change_sprites on CGB_C in Gambatte",
"m3_wx_4_change_sprites on CGB_C in Gambatte (no BIOS)",
"m3_wx_4_change_sprites on CGB_C in GBHawk",
"m3_wx_4_change_sprites on DMG in Gambatte",
"m3_wx_4_change_sprites on DMG in Gambatte (no BIOS)",
"m3_wx_4_change_sprites on DMG in GBHawk",
"m3_wx_5_change on DMG in Gambatte",
"m3_wx_5_change on DMG in Gambatte (no BIOS)",
"m3_wx_5_change on DMG in GBHawk",
"m3_wx_6_change on DMG in GBHawk",
};
public readonly string ExpectEmbedPath;
public readonly string RomEmbedPath;
public readonly CoreSetup Setup;
public readonly string TestName;
public MealybugTestCase(string testName, CoreSetup setup, string romEmbedPath, string expectEmbedPath)
{
TestName = testName;
Setup = setup;
RomEmbedPath = romEmbedPath;
ExpectEmbedPath = expectEmbedPath;
}
public readonly string DisplayName()
=> $"{TestName} on {Setup}";
}
[AttributeUsage(AttributeTargets.Method)]
private sealed class MealybugTestDataAttribute : Attribute, ITestDataSource
{
public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
{
if (!RomsArePresent) return new[] { new object?[] { MealybugTestCase.Dummy } };
var variants = new[] { ("expected.CPU_CGB_C.", ConsoleVariant.CGB_C), ("expected.CPU_CGB_D.", ConsoleVariant.CGB_D), ("expected.DMG_blob.", ConsoleVariant.DMG), ("expected.DMG_CPU_B.", ConsoleVariant.DMG_B) };
List<MealybugTestCase> testCases = new();
foreach (var item in ReflectionCache.EmbeddedResourceList(SUITE_PREFIX).Where(static item => item.EndsWith(".png")))
{
var (prefix, variant) = variants.First(kvp => item.StartsWith(kvp.Item1));
var testName = item.RemovePrefix(prefix).RemoveSuffix(".png");
var romEmbedPath = SUITE_PREFIX + $"build.ppu.{testName}.gb";
var expectEmbedPath = SUITE_PREFIX + item;
foreach (var setup in CoreSetup.ValidSetupsFor(variant)) testCases.Add(new(testName, setup, romEmbedPath, expectEmbedPath));
}
// expected value is a "no screenshot available" message
testCases.RemoveAll(static testCase =>
testCase.Setup.Variant is ConsoleVariant.CGB_C or ConsoleVariant.CGB_D
&& testCase.TestName is "m3_lcdc_win_en_change_multiple_wx" or "m3_wx_4_change" or "m3_wx_5_change" or "m3_wx_6_change");
// these are identical to CGB_C
testCases.RemoveAll(static testCase =>
testCase.Setup.Variant is ConsoleVariant.CGB_D
&& testCase.TestName is "m2_win_en_toggle" or "m3_lcdc_bg_en_change" or "m3_lcdc_bg_map_change" or "m3_lcdc_obj_en_change" or "m3_lcdc_obj_size_change" or "m3_lcdc_obj_size_change_scx" or "m3_lcdc_tile_sel_change" or "m3_lcdc_tile_sel_win_change" or "m3_lcdc_win_en_change_multiple" or "m3_lcdc_win_map_change" or "m3_scx_high_5_bits" or "m3_scx_low_3_bits" or "m3_wx_4_change" or "m3_wx_4_change_sprites" or "m3_wx_5_change" or "m3_wx_6_change");
// testCases.RemoveAll(static testCase => testCase.Setup.Variant is not ConsoleVariant.DMG); // uncomment and modify to run a subset of the test cases...
testCases.RemoveAll(static testCase => TestUtils.ShouldIgnoreCase(SUITE_ID, testCase.DisplayName())); // ...or use the global blocklist in TestUtils
return testCases.OrderBy(static testCase => testCase.DisplayName())
.Select(static testCase => new object?[] { testCase });
}
public string GetDisplayName(MethodInfo methodInfo, object?[] data)
=> $"{methodInfo.Name}({((MealybugTestCase) data[0]!).DisplayName()})";
}
private const string SUITE_ID = "Mealybug";
private const string SUITE_PREFIX = "res.mealybug_tearoom_tests_artifact.";
private static readonly bool RomsArePresent = ReflectionCache.EmbeddedResourceList().Any(static s => s.StartsWith(SUITE_PREFIX));
[ClassCleanup]
public static void AfterAll()
=> TestUtils.WriteMetricsToDisk();
[ClassInitialize]
public static void BeforeAll(TestContext ctx)
=> TestUtils.PrepareDBAndOutput(SUITE_ID);
[DataTestMethod]
[MealybugTestData]
public void RunMealybugTest(MealybugTestCase testCase)
{
TestUtils.ShortCircuitMissingRom(RomsArePresent);
var caseStr = testCase.DisplayName();
TestUtils.ShortCircuitKnownFailure(caseStr, MealybugTestCase.KnownFailures, out var knownFail);
void ExecTest(DummyFrontend fe)
{
if (testCase.Setup.CoreName is CoreNames.Gambatte)
{
// without this, exec hook triggers too early and I've decided I don't want to know why ¯\_(ツ)_/¯ --yoshi
fe.FrameAdvanceBy(5);
if (testCase.TestName is "m3_lcdc_win_map_change2") fe.FrameAdvance(); // just happens to be an outlier
}
var domain = fe.CoreAsMemDomains!.SystemBus;
Func<long> derefPC = fe.Core switch
{
Gameboy => () => domain.PeekByte((long) fe.CoreAsDebuggable!.GetCpuFlagsAndRegisters()["PC"].Value),
GBHawk gbHawk => () => domain.PeekByte(gbHawk.cpu.RegPC),
_ => throw new Exception()
};
var finished = false;
fe.CoreAsDebuggable!.MemoryCallbacks.Add(new MemoryCallback(
domain.Name,
MemoryCallbackType.Execute,
"breakpoint",
(_, _, _) =>
{
if (!finished && derefPC() is 0x40) finished = true;
},
address: null, // all addresses
mask: null));
Assert.IsTrue(fe.FrameAdvanceUntil(() => finished), "timed out waiting for exec hook");
}
var actualUnnormalised = DummyFrontend.RunAndScreenshot(
InitGBCore(testCase.Setup, $"{testCase.TestName}.gb", ReflectionCache.EmbeddedResourceStream(testCase.RomEmbedPath).ReadAllBytes()),
ExecTest);
var state = GBScreenshotsEqual(
ReflectionCache.EmbeddedResourceStream(testCase.ExpectEmbedPath),
actualUnnormalised,
knownFail,
testCase.Setup,
(SUITE_ID, caseStr),
MattCurriePaletteMap);
switch (state)
{
case TestUtils.TestSuccessState.ExpectedFailure:
Assert.Inconclusive("expected failure, verified");
break;
case TestUtils.TestSuccessState.Failure:
Assert.Fail("expected and actual screenshots differ");
break;
case TestUtils.TestSuccessState.UnexpectedSuccess:
Assert.Fail("expected and actual screenshots matched unexpectedly (this is a good thing)");
break;
}
}
}
}

View File

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Reflection;
using BizHawk.Common.IOExtensions;
using BizHawk.Emulation.Cores;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static BizHawk.Tests.Testroms.GB.GBHelper;
namespace BizHawk.Tests.Testroms.GB
{
[TestClass]
public sealed class RTC3Test
{
[AttributeUsage(AttributeTargets.Method)]
private sealed class RTC3TestData : Attribute, ITestDataSource
{
public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
{
var testCases = new[] { ConsoleVariant.CGB_C, ConsoleVariant.DMG }.SelectMany(CoreSetup.ValidSetupsFor).ToList();
// testCases.RemoveAll(static setup => setup.Variant is not ConsoleVariant.DMG); // uncomment and modify to run a subset of the test cases...
foreach (var subTest in new[] { "basic", "range", "subSecond" })
{
testCases.RemoveAll(setup => TestUtils.ShouldIgnoreCase(SUITE_ID, DisplayNameFor(setup, subTest))); // ...or use the global blocklist in TestUtils
}
return testCases.OrderBy(static setup => setup.ToString())
.Select(static setup => new object?[] { setup });
}
public string GetDisplayName(MethodInfo methodInfo, object?[] data)
=> $"{methodInfo.Name}({(CoreSetup) data[0]!})";
}
private const string ROM_EMBED_PATH = "res.rtc3test_artifact.rtc3test.gb";
private const string SUITE_ID = "RTC3Test";
private static readonly IReadOnlyCollection<string> KnownFailures = new[]
{
"",
};
private static readonly bool RomIsPresent = ReflectionCache.EmbeddedResourceList().Contains(ROM_EMBED_PATH);
[ClassCleanup]
public static void AfterAll()
=> TestUtils.WriteMetricsToDisk();
[ClassInitialize]
public static void BeforeAll(TestContext ctx)
=> TestUtils.PrepareDBAndOutput(SUITE_ID);
private static string DisplayNameFor(CoreSetup setup, string subTest)
=> $"RTC3Test.{subTest} on {setup}";
[DataTestMethod]
[RTC3TestData]
public void RunRTC3Test(CoreSetup setup)
{
TestUtils.ShortCircuitMissingRom(RomIsPresent);
TestUtils.ShortCircuitKnownFailure(new[] { "basic", "range", "subSecond" }.Select(subTest => DisplayNameFor(setup, subTest)).All(KnownFailures.Contains));
DummyFrontend fe = new(InitGBCore(setup, "rtc3test.gb", ReflectionCache.EmbeddedResourceStream(ROM_EMBED_PATH).ReadAllBytes()));
bool DoSubcaseAssertion(string subTest, Bitmap actualUnnormalised)
{
var caseStr = DisplayNameFor(setup, subTest);
var knownFail = TestUtils.IsKnownFailure(caseStr, KnownFailures);
var state = GBScreenshotsEqual(
ReflectionCache.EmbeddedResourceStream($"res.rtc3test_artifact.expected_{subTest.ToLowerInvariant()}_{(setup.Variant.IsColour() ? "cgb" : "dmg")}.png"),
actualUnnormalised,
knownFail,
setup,
(SUITE_ID, caseStr));
switch (state)
{
case TestUtils.TestSuccessState.ExpectedFailure:
Console.WriteLine("expected failure, verified");
break;
case TestUtils.TestSuccessState.Failure:
Assert.Fail("expected and actual screenshots differ");
break;
case TestUtils.TestSuccessState.Success:
return true;
case TestUtils.TestSuccessState.UnexpectedSuccess:
Assert.Fail("expected and actual screenshots matched unexpectedly (this is a good thing)");
break;
}
return false;
}
var (buttonA, buttonDown) = setup.CoreName is CoreNames.Gambatte ? ("A", "Down") : ("P1 A", "P1 Down");
fe.FrameAdvanceBy(6);
fe.SetButton(buttonA);
// fe.FrameAdvanceBy(setup.Variant.IsColour() ? 676 : 648);
fe.FrameAdvanceBy(685);
var basicPassed = DoSubcaseAssertion("basic", fe.Screenshot());
#if true
fe.Dispose();
if (!basicPassed) Assert.Inconclusive(); // for this to be false, it must have been an expected failure or execution would have stopped with an Assert.Fail call
Assert.Inconclusive("(other subtests aren't implemented)");
#else // screenshot seems to freeze emulation, or at least rendering
fe.SetButton(buttonA);
fe.FrameAdvanceBy(3);
fe.SetButton(buttonDown);
fe.FrameAdvanceBy(2);
fe.SetButton(buttonA);
fe.FrameAdvanceBy(429);
var rangePassed = DoSubcaseAssertion("range", fe.Screenshot());
fe.SetButton(buttonA);
// didn't bother TASing the remaining menu navigation because it doesn't work
// var subSecondPassed = DoSubcaseAssertion("subSecond", fe.Screenshot());
fe.Dispose();
if (!(basicPassed && rangePassed /*&& subSecondPassed*/)) Assert.Inconclusive(); // for one of these to be false, it must have been an expected failure or execution would have stopped with an Assert.Fail call
#endif
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using BizHawk.Common;
using BizHawk.Common.PathExtensions;
using ImageMagick;
namespace BizHawk.Tests.Testroms.GB
{
public static class ImageUtils
{
public static Bitmap AsBitmap(this Image img)
=> img as Bitmap ?? new Bitmap(img, img.Size);
/// <param name="fileExt">w/o leading '.'</param>
private static (string Expect, string Actual, string Glob) GenFilenames((string Suite, string Case) id, string fileExt = "png")
{
var prefix = $"{id.Suite}/{id.Case.GetHashCode():X8}"; // hashcode of string sadly not stable
var suffix = $"{id.Case.RemoveInvalidFileSystemChars().Replace(' ', '_')}.{fileExt}";
return ($"{prefix}_expect_{suffix}", $"{prefix}_actual_{suffix}", $"{prefix}_*_{suffix}");
}
public static int GetRawPixel(this Bitmap b, int x, int y)
=> b.GetPixel(x, y).ToArgb() & 0xFFFFFF;
/// <param name="map">ints are ARGB as <see cref="System.Drawing.Color.ToArgb"/></param>
public static Bitmap PaletteSwap(Image img, IReadOnlyDictionary<int, int> map)
{
int Lookup(int c)
=> map.TryGetValue(c, out var c1) ? c1 : c;
var b = ((Image) img.Clone()).AsBitmap();
for (int y = 0, ly = b.Height; y < ly; y++) for (int x = 0, lx = b.Width; x < lx; x++)
{
b.SetPixel(x, y, Color.FromArgb(0xFF, Color.FromArgb(Lookup(b.GetRawPixel(x, y)))));
}
return b;
}
public static void PrintPalette(Image imgA, string labelA, Image imgB, string labelB)
{
static IReadOnlySet<int> CollectPalette(Image img)
{
var b = img.AsBitmap();
HashSet<int> paletteE = new();
for (int y = 0, ly = b.Height; y < ly; y++) for (int x = 0, lx = b.Width; x < lx; x++)
{
paletteE.Add(b.GetRawPixel(x, y));
}
return paletteE;
}
static string F(Image img)
=> string.Join(", ", CollectPalette(img).Select(static i => $"{i:X6}"));
Console.WriteLine($"palette of {labelA}:\n{F(imgA)}\npalette of {labelB}:\n{F(imgB)}");
}
public static void SaveScreenshot(Image img, (string Suite, string Case) id)
{
var filename = GenFilenames(id).Actual;
img.ToMagickImage().Write(filename, MagickFormat.Png);
Console.WriteLine($"screenshot saved for {id.Case} as {filename}");
}
/// <remarks>initially added this as a workaround for various bugs in <c>System.Drawing.*</c> on Linux, but this also happens to be faster on Windows</remarks>
public static TestUtils.TestSuccessState ScreenshotsEqualMagickDotNET(Stream expectFile, Image actual, bool expectingNotEqual, (string Suite, string Case) id)
{
var actualIM = actual.ToMagickImage();
MagickImage expectIM = new(expectFile);
var error = expectIM.Compare(actualIM, ErrorMetric.Absolute);
var state = TestUtils.SuccessState(error == 0.0, expectingNotEqual);
if (!SkipFileIO(state))
{
var (filenameExpect, filenameActual, filenameGlob) = GenFilenames(id);
actualIM.Write(filenameActual, MagickFormat.Png);
expectIM.Write(filenameExpect, MagickFormat.Png);
Console.WriteLine($"screenshots saved for {id.Case} as {filenameGlob} (difference: {error})");
}
return state;
}
public static bool SkipFileIO(TestUtils.TestSuccessState state)
#if SAVE_IMAGES_ON_FAIL && SAVE_IMAGES_ON_PASS // run with env. var BIZHAWKTEST_SAVE_IMAGES=all
=> false;
#elif SAVE_IMAGES_ON_FAIL // run without extra env. var, or with env. var BIZHAWKTEST_SAVE_IMAGES=failures
=> state is TestUtils.TestSuccessState.Success;
#elif SAVE_IMAGES_ON_PASS // normally inaccessible
=> state is not TestUtils.TestSuccessState.Success;
#else // run with env. var BIZHAWKTEST_SAVE_IMAGES=none
=> true;
#endif
public static MagickImage ToMagickImage(this Image img)
{
MemoryStream ms = new();
img.Save(ms, OSTailoredCode.IsUnixHost ? ImageFormat.Bmp : ImageFormat.Png);
ms.Position = 0;
return new(ms);
}
}
}

View File

@ -0,0 +1,3 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: TestDataSourceDiscovery(TestDataSourceDiscoveryOption.DuringExecution)]

View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using BizHawk.Common;
using BizHawk.Emulation.Common;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace BizHawk.Tests.Testroms.GB
{
public static class TestUtils
{
public enum TestSuccessState { ExpectedFailure, Failure, Success, UnexpectedSuccess }
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint SetDllDirectory(string lpPathName);
private static readonly SortedSet<string> _initialised = new();
public static bool IsKnownFailure(string caseStr, IReadOnlyCollection<string> knownFailures)
=> knownFailures is string[] a
? Array.BinarySearch(a, caseStr) >= 0
: knownFailures.Contains(caseStr);
public static void PrepareDBAndOutput(string suiteID)
{
if (_initialised.Contains(suiteID)) return;
if (_initialised.Count == 0)
{
Database.InitializeDatabase(Path.Combine(".", "gamedb", "gamedb.txt"), silent: true); // runs in the background; required for Database.GetGameInfo calls
if (!OSTailoredCode.IsUnixHost) SetDllDirectory(Path.Combine("..", "output", "dll")); // on Linux, this is done by the shell script with the env. var. LD_LIBRARY_PATH
}
_initialised.Add(suiteID);
DirectoryInfo di = new(suiteID);
if (di.Exists) di.Delete(recursive: true);
di.Create();
}
[Conditional("SKIP_KNOWN_FAILURES")] // run with env. var BIZHAWKTEST_RUN_KNOWN_FAILURES=1
public static void ShortCircuitKnownFailure(bool knownFail)
{
if (knownFail) Assert.Inconclusive("short-circuiting this test which is known to fail");
}
public static void ShortCircuitKnownFailure(string caseStr, IReadOnlyCollection<string> knownFailures, out bool isKnownFailure)
{
isKnownFailure = IsKnownFailure(caseStr, knownFailures);
ShortCircuitKnownFailure(isKnownFailure);
}
public static void ShortCircuitMissingRom(bool isPresent)
{
if (!isPresent) Assert.Inconclusive("missing file(s)");
}
/// <remarks>programmatically veto any test cases by modifying this method</remarks>
public static bool ShouldIgnoreCase(string suiteID, string caseStr)
{
// if (caseStr.Contains("timing")) return true;
return false;
}
public static TestSuccessState SuccessState(bool didPass, bool shouldNotPass)
=> shouldNotPass
? didPass ? TestSuccessState.UnexpectedSuccess : TestSuccessState.ExpectedFailure
: didPass ? TestSuccessState.Success : TestSuccessState.Failure;
public static void WriteMetricsToDisk()
=> File.WriteAllText("total_frames.txt", $"emulated {DummyFrontend.TotalFrames} frames total");
}
}

View File

@ -0,0 +1,75 @@
Before building, testroms and firmware need to be placed under `/res` in this project.
You *should* be able to omit any suite or firmware and the relevant cases will be skipped.
Firmware needs to be manually copied into a `fw` dir;
testroms need to be copied into a separate dir per suite, with a hierarchy matching the CI artifacts of [this repo](https://gitlab.com/tasbot/libre-roms-ci).
On Linux, run `/res/download_from_ci.sh` to automatically download and extract said artifacts.
On Windows, run the same script in WSL, or do it manually (because Yoshi can't be bothered porting the script to PowerShell).
All told, the expected directory structure is:
```
res
├─ BullyGB_artifact
├─ cgb-acid-hell_artifact
├─ cgb-acid2_artifact
├─ dmg-acid2_artifact
├─ fw
│ ├─ GB__World__DMG.bin
│ └─ GBC__World__CGB.bin
├─ mealybug-tearoom-tests_artifact
└─ rtc3test_artifact
```
As with EmuHawk, the target framework and configuration for all the BizHawk project deps is dictated by this project. That means .NET Standard 2.0, or .NET 5 if the project supports it.
To build and run the tests in `Release` configuration (or `Debug` if you need that for some reason):
- On Linux, run `run_tests_release.sh` or `run_tests_debug.sh`.
- On Windows, pass `-c Release` to `dotnet test` (must `cd` to this project). Omitting `-c` will use `Debug`.
> You can at this point run the tests, but you should probably keep reading to see your options.
To run only some suites, comment out applications of the `[DataTestMethod]` attribute in the source. (Or applications of `[TestClass]`.)
You can also disable individual test cases programmatically by modifying `TestUtils.ShouldIgnoreCase`
note that "ignored" here means cases are completely removed, and do not count as "skipped".
By default, known failures are counted as "skipped" *without actually running them*.
Set the env. var `BIZHAWKTEST_RUN_KNOWN_FAILURES=1` to run them as well. They will count as "skipped" if they fail, or "failed" if they succeed unexpectedly.
On Linux, all cases for unavailable cores (N/A currently) are counted as "skipped".
Screenshots may be saved under `/test_output/<suite>` **in the repo**.
For ease of typing, a random prefix is chosen for each case e.g. `DEADBEEF_{expected,actual}_*.png`. This is included in stdout (Windows users, see below for how to enable stdout).
The env. var `BIZHAWKTEST_SAVE_IMAGES` determines when to save screenshots (usually an expect/actual pair) to disk.
- With `BIZHAWKTEST_SAVE_IMAGES=all`, all screenshots are saved.
- With `BIZHAWKTEST_SAVE_IMAGES=failures` (the default), only screenshots of failed tests are saved.
- With `BIZHAWKTEST_SAVE_IMAGES=none`, screenshots are never saved.
Test results are output using the logger(s) specified on the command-line.
(Without the `console` logger, the results are summarised in the console, but prints to stdout are not shown.)
- On Linux, the shell scripts add the `console` and `junit` (to file, for GitLab CI) loggers.
- On Windows, pass `-l "console;verbosity=detailed"` to `dotnet`.
> Note that the results and stdout for each test case are not printed immediately.
> Cases are grouped by test method, and once the set of test cases is finished executing, the outputs are sent to the console all at once.
Linux examples:
```sh
# default: simple regression testing, all test suites, saving failures to disk
./run_tests_release.sh
# every test from every suite, not saving anything to disk (as might be used in CI)
BIZHAWKTEST_RUN_KNOWN_FAILURES=1 BIZHAWKTEST_SAVE_IMAGES=none ./run_tests_release.sh
```
Windows examples:
```pwsh
# reminder that if you have WSL, you can use that to run /res/download_from_ci.sh first
# default: simple regression testing, all test suites, saving failures to disk
dotnet test -c Release -l "console;verbosity=detailed"
# same as Linux CI example
$Env:BIZHAWKTEST_RUN_KNOWN_FAILURES = 1
$Env:BIZHAWKTEST_SAVE_IMAGES = "all"
dotnet test -c Release -l "console;verbosity=detailed"
```
Summary of `BIZHAWKTEST_RUN_KNOWN_FAILURES=1 ./run_tests_release.sh` should read 86 passed / 118 skipped / 0 failed.

View File

@ -0,0 +1,4 @@
#!/bin/sh
set -e
cd "$(dirname "$(realpath "$0")")"
../../BizHawk.Tests.Testroms.GB/.download_from_ci.sh BullyGB cgb-acid-hell cgb-acid2 dmg-acid2 mealybug-tearoom-tests rtc3test

View File

@ -0,0 +1,2 @@
#!/bin/sh
cd "$(dirname "$(realpath "$0")")" && ../BizHawk.Tests.Testroms.GB/.run_tests_with_configuration.sh "Debug" "$@"

View File

@ -0,0 +1,2 @@
#!/bin/sh
cd "$(dirname "$(realpath "$0")")" && ../BizHawk.Tests.Testroms.GB/.run_tests_with_configuration.sh "Release" "$@"