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.Common/VersionInfo.gen.cs
|
||||||
|
|
||||||
|
/src/BizHawk.Tests*/res/*_artifact
|
||||||
|
/src/BizHawk.Tests*/res/fw
|
||||||
|
|
||||||
/Build/*.vshost.*
|
/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