mirror of
https://github.com/SourMesen/Mesen2.git
synced 2024-06-22 22:22:51 -04:00
Tests: Ability to record/replay tests
This commit is contained in:
parent
cfc870c897
commit
5a5aa53874
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
87
NewUI/Utilities/RomTestHelper.cs
Normal file
87
NewUI/Utilities/RomTestHelper.cs
Normal 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());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue