Add regression tests for GB/C using various testroms
This commit is contained in:
parent
934a3ae266
commit
940dc69ae5
|
@ -31,7 +31,8 @@
|
|||
|
||||
/src/BizHawk.Common/VersionInfo.gen.cs
|
||||
|
||||
|
||||
/src/BizHawk.Tests*/res/*_artifact
|
||||
/src/BizHawk.Tests*/res/fw
|
||||
|
||||
/Build/*.vshost.*
|
||||
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
[assembly: TestDataSourceDiscovery(TestDataSourceDiscoveryOption.DuringExecution)]
|
|
@ -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.
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
cd "$(dirname "$(realpath "$0")")"
|
||||
../../BizHawk.Tests.Testroms.GB/.download_from_ci.sh Gambatte-testroms
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
cd "$(dirname "$(realpath "$0")")" && ../BizHawk.Tests.Testroms.GB/.run_tests_with_configuration.sh "Debug" "$@"
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
cd "$(dirname "$(realpath "$0")")" && ../BizHawk.Tests.Testroms.GB/.run_tests_with_configuration.sh "Release" "$@"
|
|
@ -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
|
|
@ -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" "$@"
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
[assembly: TestDataSourceDiscovery(TestDataSourceDiscoveryOption.DuringExecution)]
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
cd "$(dirname "$(realpath "$0")")" && ../BizHawk.Tests.Testroms.GB/.run_tests_with_configuration.sh "Debug" "$@"
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
cd "$(dirname "$(realpath "$0")")" && ../BizHawk.Tests.Testroms.GB/.run_tests_with_configuration.sh "Release" "$@"
|
Loading…
Reference in New Issue