Tests: Ability to record/replay tests

This commit is contained in:
Sour 2023-01-24 05:20:42 -05:00
parent cfc870c897
commit 5a5aa53874
9 changed files with 214 additions and 40 deletions

View file

@ -11,6 +11,8 @@
std::unordered_map<uint32_t, GameInfo> GameDatabase::_gameDatabase;
bool GameDatabase::_enabled = true;
bool GameDatabase::_initialized = false;
SimpleLock GameDatabase::_loadLock;
template<typename T>
T GameDatabase::ToInt(string value)
@ -77,10 +79,14 @@ void GameDatabase::LoadGameDb(std::istream &db)
void GameDatabase::InitDatabase()
{
if(_gameDatabase.size() == 0) {
string dbPath = FolderUtilities::CombinePath(FolderUtilities::GetHomeFolder(), "MesenNesDB.txt");
ifstream db(dbPath, ios::in | ios::binary);
LoadGameDb(db);
if(!_initialized) {
auto lock = _loadLock.AcquireSafe();
if(!_initialized) {
string dbPath = FolderUtilities::CombinePath(FolderUtilities::GetHomeFolder(), "MesenNesDB.txt");
ifstream db(dbPath, ios::in | ios::binary);
LoadGameDb(db);
_initialized = true;
}
}
}

View file

@ -2,6 +2,7 @@
#include "pch.h"
#include <unordered_map>
#include "NES/RomData.h"
#include "Utilities/SimpleLock.h"
struct NesHeader;
@ -10,6 +11,8 @@ class GameDatabase
private:
static std::unordered_map<uint32_t, GameInfo> _gameDatabase;
static bool _enabled;
static bool _initialized;
static SimpleLock _loadLock;
template<typename T> static T ToInt(string value);

View file

@ -161,11 +161,6 @@ bool MesenMovie::Play(VirtualFile &file)
_controlManager = _emu->GetConsole()->GetControlManager();
if(_forTest) {
//TODOv2 to validate test behavior
_controlManager->RegisterInputProvider(this);
}
LoadCheats();
stringstream saveStateData;

View file

@ -13,9 +13,10 @@
#include "Utilities/ZipReader.h"
#include "Utilities/ArchiveReader.h"
RecordedRomTest::RecordedRomTest(Emulator* emu)
RecordedRomTest::RecordedRomTest(Emulator* emu, bool inBackground)
{
_emu = emu;
_inBackground = inBackground;
Reset();
}
@ -64,8 +65,11 @@ void RecordedRomTest::ValidateFrame()
if(memcmp(_screenshotHashes.front(), md5Hash, 16) != 0) {
_badFrameCount++;
_isLastFrameGood = false;
//_console->BreakIfDebugging();
}
} else {
_isLastFrameGood = true;
}
if(_currentCount == 0 && _repetitionCount.empty()) {
//End of test
@ -121,10 +125,10 @@ void RecordedRomTest::Record(string filename, bool reset)
_emu->GetSettings()->GetVideoConfig().DisableFrameSkipping = true;
//TODOv2 - remove snes-specific code
SnesConfig& snesCfg = _emu->GetSettings()->GetSnesConfig();
snesCfg.RamPowerOnState = RamState::AllZeros;
_emu->GetSettings()->SetSnesConfig(snesCfg);
_emu->GetSettings()->GetSnesConfig().RamPowerOnState = RamState::AllZeros;
_emu->GetSettings()->GetNesConfig().RamPowerOnState = RamState::AllZeros;
_emu->GetSettings()->GetGameboyConfig().RamPowerOnState = RamState::AllZeros;
_emu->GetSettings()->GetPcEngineConfig().RamPowerOnState = RamState::AllZeros;
//Start recording movie alongside with screenshots
RecordMovieOptions options;
@ -138,19 +142,32 @@ void RecordedRomTest::Record(string filename, bool reset)
}
}
int32_t RecordedRomTest::Run(string filename)
RomTestResult RecordedRomTest::Run(string filename)
{
RomTestResult result = {};
_emu->GetNotificationManager()->RegisterNotificationListener(shared_from_this());
EmuSettings* settings = _emu->GetSettings();
string testName = FolderUtilities::GetFilename(filename, false);
VirtualFile testMovie(filename, "TestMovie.msm");
VirtualFile testRom(filename, "TestRom.sfc");
ZipReader zipReader;
zipReader.LoadArchive(filename);
vector<string> files = zipReader.GetFileList();
string romFile = "";
for(string& file : files) {
if(file.length() > 7 && file.substr(0, 7) == "TestRom") {
romFile = file;
}
}
if(romFile.empty()) {
result.ErrorCode = -4;
return result;
}
VirtualFile testMovie(filename, "TestMovie.mmo");
VirtualFile testRom(filename, romFile);
stringstream testData;
zipReader.GetStream("TestData.mrt", testData);
@ -159,7 +176,8 @@ int32_t RecordedRomTest::Run(string filename)
testData.read((char*)&header, 3);
if(memcmp((char*)&header, "MRT", 3) != 0) {
//Invalid test file
return false;
result.ErrorCode = -3;
return result;
}
Reset();
@ -180,36 +198,47 @@ int32_t RecordedRomTest::Run(string filename)
_currentCount = _repetitionCount.front();
_repetitionCount.pop_front();
_emu->GetSettings()->GetVideoConfig().DisableFrameSkipping = true;
if(testName.compare("demo_pal") == 0 || testName.substr(0, 4).compare("pal_") == 0) {
settings->GetNesConfig().Region = ConsoleRegion::Pal;
} else {
settings->GetNesConfig().Region = ConsoleRegion::Auto;
}
//TODOv2 - remove snes-specific code
SnesConfig& snesCfg = _emu->GetSettings()->GetSnesConfig();
snesCfg.RamPowerOnState = RamState::AllZeros;
_emu->GetSettings()->SetSnesConfig(snesCfg);
settings->GetVideoConfig().DisableFrameSkipping = true;
settings->GetSnesConfig().RamPowerOnState = RamState::AllZeros;
settings->GetNesConfig().RamPowerOnState = RamState::AllZeros;
settings->GetGameboyConfig().RamPowerOnState = RamState::AllZeros;
settings->GetPcEngineConfig().RamPowerOnState = RamState::AllZeros;
_emu->Lock();
//Start playing movie
if(_emu->LoadRom(testRom, VirtualFile(""))) {
settings->SetFlag(EmulationFlags::MaximumSpeed);
_emu->GetMovieManager()->Play(testMovie, true);
settings->SetFlag(EmulationFlags::MaximumSpeed);
_runningTest = true;
_emu->Unlock();
_signal.Wait();
_emu->Stop(false);
_emu->Stop(!_inBackground);
_runningTest = false;
} else {
//Something went wrong when loading the rom
_emu->Unlock();
return -2;
result.ErrorCode = -2;
return result;
}
settings->ClearFlag(EmulationFlags::MaximumSpeed);
return _badFrameCount;
result.ErrorCode = _badFrameCount;
result.State = _badFrameCount == 0 ? RomTestState::Passed : (_isLastFrameGood ? RomTestState::PassedWithWarnings : RomTestState::Failed);
return result;
}
return -1;
result.ErrorCode = -1;
return result;
}
void RecordedRomTest::Stop()
@ -250,10 +279,10 @@ void RecordedRomTest::Save()
std::remove(mrtFilename.c_str());
string mmoFilename = FolderUtilities::CombinePath(FolderUtilities::GetFolderName(_filename), FolderUtilities::GetFilename(_filename, false) + ".mmo");
writer.AddFile(mmoFilename, "TestMovie.msm");
writer.AddFile(mmoFilename, "TestMovie.mmo");
std::remove(mmoFilename.c_str());
writer.AddFile(_emu->GetRomInfo().RomFile.GetFilePath(), "TestRom.sfc");
writer.AddFile(_emu->GetRomInfo().RomFile.GetFilePath(), "TestRom" + _emu->GetRomInfo().RomFile.GetFileExtension());
writer.Save();

View file

@ -8,14 +8,29 @@
class VirtualFile;
class Emulator;
enum class RomTestState
{
Failed,
Passed,
PassedWithWarnings
};
struct RomTestResult
{
RomTestState State;
int32_t ErrorCode;
};
class RecordedRomTest : public INotificationListener, public std::enable_shared_from_this<RecordedRomTest>
{
private:
Emulator* _emu;
bool _inBackground = false;
bool _recording = false;
bool _runningTest = false;
int _badFrameCount = 0;
bool _isLastFrameGood = false;
uint8_t _previousHash[16] = {};
std::deque<uint8_t*> _screenshotHashes;
@ -34,11 +49,11 @@ private:
void Save();
public:
RecordedRomTest(Emulator* console = nullptr);
RecordedRomTest(Emulator* console, bool inBackground);
virtual ~RecordedRomTest();
void ProcessNotification(ConsoleNotificationType type, void* parameter) override;
void Record(string filename, bool reset);
int32_t Run(string filename);
RomTestResult Run(string filename);
void Stop();
};

View file

@ -7,22 +7,22 @@ shared_ptr<RecordedRomTest> _recordedRomTest;
extern "C"
{
DllExport int32_t __stdcall RunRecordedTest(char* filename, bool inBackground)
DllExport RomTestResult __stdcall RunRecordedTest(char* filename, bool inBackground)
{
if(inBackground) {
unique_ptr<Emulator> emu(new Emulator());
emu->Initialize();
shared_ptr<RecordedRomTest> romTest(new RecordedRomTest(emu.get()));
shared_ptr<RecordedRomTest> romTest(new RecordedRomTest(emu.get(), true));
return romTest->Run(filename);
} else {
shared_ptr<RecordedRomTest> romTest(new RecordedRomTest(_emu.get()));
shared_ptr<RecordedRomTest> romTest(new RecordedRomTest(_emu.get(), false));
return romTest->Run(filename);
}
}
DllExport void __stdcall RomTestRecord(char* filename, bool reset)
{
_recordedRomTest.reset(new RecordedRomTest(_emu.get()));
_recordedRomTest.reset(new RecordedRomTest(_emu.get(), false));
_recordedRomTest->Record(filename, reset);
}

View file

@ -11,9 +11,22 @@ namespace Mesen.Interop
{
private const string DllPath = EmuApi.DllName;
[DllImport(DllPath)] public static extern Int32 RunRecordedTest([MarshalAs(UnmanagedType.LPUTF8Str)]string filename, [MarshalAs(UnmanagedType.I1)]bool inBackground);
[DllImport(DllPath)] public static extern RomTestResult RunRecordedTest([MarshalAs(UnmanagedType.LPUTF8Str)]string filename, [MarshalAs(UnmanagedType.I1)]bool inBackground);
[DllImport(DllPath)] public static extern void RomTestRecord([MarshalAs(UnmanagedType.LPUTF8Str)]string filename, [MarshalAs(UnmanagedType.I1)]bool reset);
[DllImport(DllPath)] public static extern void RomTestStop();
[DllImport(DllPath)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool RomTestRecording();
}
public struct RomTestResult
{
public RomTestState State;
public Int32 ErrorCode;
}
public enum RomTestState
{
Failed,
Passed,
PassedWithWarnings
}
}

View file

@ -0,0 +1,87 @@
using Avalonia.Threading;
using Mesen.Config;
using Mesen.Interop;
using Mesen.Windows;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Mesen.Utilities
{
public static class RomTestHelper
{
public static async void RunTest()
{
string? filename = await FileDialogHelper.OpenFile(ConfigManager.TestFolder, null, "mtp");
if(filename != null) {
RomTestResult result = TestApi.RunRecordedTest(filename, false);
string msg = "[Test] " + result.State.ToString();
if(result.State != RomTestState.Passed) {
msg += ": (" + result.ErrorCode.ToString() + ")";
}
await MessageBox.Show(null, msg, "", MessageBoxButtons.OK, result.State == RomTestState.Failed ? MessageBoxIcon.Error : MessageBoxIcon.Info);
}
}
public static void RecordTest()
{
string filename = Path.Join(ConfigManager.TestFolder, EmuApi.GetRomInfo().GetRomName() + ".mtp");
TestApi.RomTestRecord(filename, true);
}
public static void RunAllTests()
{
Task.Run(() => {
ConcurrentDictionary<string, RomTestResult> results = new();
List<string> testFiles = Directory.EnumerateFiles(ConfigManager.TestFolder, "*.mtp", SearchOption.AllDirectories).ToList();
Parallel.ForEach(testFiles, new ParallelOptions() { MaxDegreeOfParallelism = 6 }, (string testFile) => {
string entryName = testFile.Substring(ConfigManager.TestFolder.Length);
results[entryName] = TestApi.RunRecordedTest(testFile, true);
});
EmuApi.WriteLogEntry("==================");
List<string> failedTests = new List<string>();
List<string> entries = results.Keys.ToList();
entries.Sort();
foreach(var entry in entries) {
RomTestResult result = results[entry];
string msg = "[Test] " + result.State.ToString() + ": " + entry;
if(result.State != RomTestState.Passed) {
msg +=" (" + result.ErrorCode.ToString() + ")";
}
EmuApi.WriteLogEntry(msg);
if(result.State == RomTestState.Failed) {
failedTests.Add(entry);
}
}
EmuApi.WriteLogEntry("==================");
if(failedTests.Count > 0) {
EmuApi.WriteLogEntry("Tests passed: " + (testFiles.Count - failedTests.Count));
EmuApi.WriteLogEntry("Tests failed: " + failedTests.Count);
foreach(string failedTest in failedTests) {
EmuApi.WriteLogEntry(" Failed: " + failedTest);
}
} else {
EmuApi.WriteLogEntry("All " + testFiles.Count + " tests passed!");
}
EmuApi.WriteLogEntry("==================");
Dispatcher.UIThread.Post(() => {
ApplicationHelper.GetOrCreateUniqueWindow<LogWindow>(null, () => new LogWindow());
});
});
}
}
}

View file

@ -39,6 +39,8 @@ namespace Mesen.Windows
private MainMenuView _mainMenu;
private CommandLineHelper? _cmdLine;
private bool _testModeEnabled;
//Used to suppress key-repeat keyup events on Linux
private Dictionary<Key, IDisposable> _pendingKeyUpEvents = new();
private bool _isLinux = false;
@ -51,6 +53,7 @@ namespace Mesen.Windows
public MainWindow()
{
_testModeEnabled = System.Diagnostics.Debugger.IsAttached;
_isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
DataContext = new MainWindowViewModel();
@ -447,8 +450,31 @@ namespace Mesen.Windows
}
}
private bool ProcessTestModeShortcuts(Key key)
{
if(key == Key.F1) {
if(TestApi.RomTestRecording()) {
TestApi.RomTestStop();
} else {
RomTestHelper.RecordTest();
}
return true;
} else if(key == Key.F2) {
RomTestHelper.RunTest();
return true;
} else if(key == Key.F3) {
RomTestHelper.RunAllTests();
return true;
}
return false;
}
private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
{
if(_testModeEnabled && e.KeyModifiers == KeyModifiers.Alt && ProcessTestModeShortcuts(e.Key)) {
return;
}
if(e.Key != Key.None) {
if(_isLinux && _pendingKeyUpEvents.TryGetValue(e.Key, out IDisposable? cancelTimer)) {
//Cancel any pending key up event