diff --git a/CMakeLists.txt b/CMakeLists.txt index 598b3bdb..819a744a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,3 +111,12 @@ add_subdirectory(src) if (BUILD_QT_SDL) add_subdirectory(src/frontend/qt_sdl) endif() + +option(MELONDS_BUILD_TESTS "Build the melonDS test suite." OFF) + +if (MELONDS_BUILD_TESTS) + include(CTest) + message(STATUS "Enabling melonDS test suite.") + enable_testing() + add_subdirectory(test) +endif() \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..4b558272 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,65 @@ +cmake_policy(SET CMP0110 NEW) + +if (NOT NDS_ROM) + message(WARNING "NDS_ROM must be set to the path of an NDS ROM; tests that require one won't run.") + set(NDS_ROM "NDS_ROM-NOTFOUND" CACHE FILEPATH "Path to an NDS ROM" FORCE) +else() + message(DEBUG "NDS_ROM: ${NDS_ROM}") +endif() + +include(CMakePrintHelpers) + +function(add_melonds_test) + set(options WILL_FAIL ARM7_BIOS ARM9_BIOS ARM7_DSI_BIOS ARM9_DSI_BIOS NDS_FIRMWARE DSI_FIRMWARE DSI_NAND DISABLED) + set(oneValueArgs TARGET NAME ROM) + set(multiValueArgs ARGS LABEL) + cmake_parse_arguments(PARSE_ARGV 0 MELONDS_TEST "${options}" "${oneValueArgs}" "${multiValueArgs}") + + cmake_print_variables(MELONDS_TEST_TARGET MELONDS_TEST_NAME MELONDS_TEST_ROM MELONDS_TEST_ARGS MELONDS_TEST_LABEL MELONDS_TEST_WILL_FAIL MELONDS_TEST_DISABLED) + add_test( + NAME "${MELONDS_TEST_NAME}" + COMMAND "${MELONDS_TEST_TARGET}" + ${MELONDS_TEST_ARGS} + COMMAND_EXPAND_LISTS + ) + + if (MELONDS_TEST_ROM) + list(APPEND REQUIRED_FILES "${MELONDS_TEST_ROM}") + endif() + + macro(expose_system_file SYSFILE) + if (MELONDS_TEST_${SYSFILE}) + list(APPEND REQUIRED_FILES "${${SYSFILE}}") + list(APPEND ENVIRONMENT "${SYSFILE}=${${SYSFILE}}") + endif() + endmacro() + + expose_system_file(ARM7_BIOS) + expose_system_file(ARM9_BIOS) + expose_system_file(ARM7_DSI_BIOS) + expose_system_file(ARM9_DSI_BIOS) + expose_system_file(NDS_FIRMWARE) + expose_system_file(DSI_FIRMWARE) + expose_system_file(DSI_NAND) + + set_tests_properties("${MELONDS_TEST_NAME}" PROPERTIES LABELS "${MELONDS_TEST_LABEL}") # This is already a list + set_tests_properties("${MELONDS_TEST_NAME}" PROPERTIES ENVIRONMENT "${ENVIRONMENT}") + set_tests_properties("${MELONDS_TEST_NAME}" PROPERTIES REQUIRED_FILES "${REQUIRED_FILES}") + + if (MELONDS_TEST_WILL_FAIL) + set_tests_properties("${MELONDS_TEST_NAME}" PROPERTIES WILL_FAIL TRUE) + endif() + + if (MELONDS_TEST_DISABLED) + set_tests_properties("${MELONDS_TEST_NAME}" PROPERTIES DISABLED TRUE) + endif() +endfunction() + +function(add_test_executable TEST_PROGRAM) + add_executable("${TEST_PROGRAM}" "${TEST_PROGRAM}.cpp") + target_link_libraries("${TEST_PROGRAM}" PRIVATE core melonDS-tests-common) + target_include_directories("${TEST_PROGRAM}" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../src") +endfunction() + +add_subdirectory(common) +add_subdirectory(cases) diff --git a/test/cases/CMakeLists.txt b/test/cases/CMakeLists.txt new file mode 100644 index 00000000..3c776dd5 --- /dev/null +++ b/test/cases/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(basic) \ No newline at end of file diff --git a/test/cases/basic/CMakeLists.txt b/test/cases/basic/CMakeLists.txt new file mode 100644 index 00000000..ba94b669 --- /dev/null +++ b/test/cases/basic/CMakeLists.txt @@ -0,0 +1,22 @@ +# Test executables must be defined separately from the actual tests, +# as the same test executable might be used for multiple tests +# (with different parameters each time). + +add_test_executable(NDSCreated) +add_test_executable(MultipleNDSCreated) +add_test_executable(MultipleNDSCreatedInDifferentOrder) + +add_melonds_test( + NAME "NDS is created and destroyed" + TARGET NDSCreated +) + +add_melonds_test( + NAME "Multiple NDS systems are created and destroyed" + TARGET MultipleNDSCreated +) + +add_melonds_test( + NAME "Multiple NDS systems are created and destroyed in different orders" + TARGET MultipleNDSCreatedInDifferentOrder +) \ No newline at end of file diff --git a/test/cases/basic/MultipleNDSCreated.cpp b/test/cases/basic/MultipleNDSCreated.cpp new file mode 100644 index 00000000..a9d07e8a --- /dev/null +++ b/test/cases/basic/MultipleNDSCreated.cpp @@ -0,0 +1,28 @@ +/* + Copyright 2016-2023 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#include "NDS.h" + +int main() +{ + // volatile so it's not optimized out + volatile auto nds1 = std::make_unique(); + volatile auto nds2 = std::make_unique(); + volatile auto nds3 = std::make_unique(); + volatile auto nds4 = std::make_unique(); +} \ No newline at end of file diff --git a/test/cases/basic/MultipleNDSCreatedInDifferentOrder.cpp b/test/cases/basic/MultipleNDSCreatedInDifferentOrder.cpp new file mode 100644 index 00000000..2acf0ff5 --- /dev/null +++ b/test/cases/basic/MultipleNDSCreatedInDifferentOrder.cpp @@ -0,0 +1,34 @@ +/* + Copyright 2016-2023 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#include +#include "NDS.h" + +int main() +{ + // volatile so it's not optimized out + auto nds1 = std::make_unique(); + auto nds2 = std::make_unique(); + auto nds3 = std::make_unique(); + auto nds4 = std::make_unique(); + + nds3 = nullptr; + nds1 = nullptr; + nds4 = nullptr; + nds2 = nullptr; +} \ No newline at end of file diff --git a/test/cases/basic/NDSCreated.cpp b/test/cases/basic/NDSCreated.cpp new file mode 100644 index 00000000..8f559deb --- /dev/null +++ b/test/cases/basic/NDSCreated.cpp @@ -0,0 +1,28 @@ +/* + Copyright 2016-2023 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#include +#include "NDS.h" + +int main() +{ + // volatile so it's not optimized out + volatile auto nds = std::make_unique(); + + // NDS is probably waaaay too big for the stack +} \ No newline at end of file diff --git a/test/common/CMakeLists.txt b/test/common/CMakeLists.txt new file mode 100644 index 00000000..ae3acea8 --- /dev/null +++ b/test/common/CMakeLists.txt @@ -0,0 +1,13 @@ +include(FixInterfaceIncludes) + +add_library(melonDS-tests-common STATIC + Platform.cpp + TestCommon.cpp + TestCommon.h +) + +# The test program is built with C++20 so we can use std::counting_semaphore +set_target_properties(melonDS-tests-common PROPERTIES CXX_STANDARD 20) + +target_link_libraries(melonDS-tests-common PRIVATE core) +target_include_directories(melonDS-tests-common PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../src") \ No newline at end of file diff --git a/test/common/Platform.cpp b/test/common/Platform.cpp new file mode 100644 index 00000000..9d8b8eb8 --- /dev/null +++ b/test/common/Platform.cpp @@ -0,0 +1,316 @@ +/* + Copyright 2016-2023 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#include "Platform.h" + +#include +#include +#include +#include + +namespace melonDS::Platform +{ +void SignalStop(StopReason reason) {} +int InstanceID() { return 0; } + +std::string InstanceFileSuffix() { return ""; } + +constexpr char AccessMode(FileMode mode, bool file_exists) +{ + if (!(mode & FileMode::Write)) + // If we're only opening the file for reading... + return 'r'; + + if (mode & (FileMode::NoCreate)) + // If we're not allowed to create a new file... + return 'r'; // Open in "r+" mode (IsExtended will add the "+") + + if ((mode & FileMode::Preserve) && file_exists) + // If we're not allowed to overwrite a file that already exists... + return 'r'; // Open in "r+" mode (IsExtended will add the "+") + + return 'w'; +} + +constexpr bool IsExtended(FileMode mode) +{ + // fopen's "+" flag always opens the file for read/write + return (mode & FileMode::ReadWrite) == FileMode::ReadWrite; +} + +static std::string GetModeString(FileMode mode, bool file_exists) +{ + std::string modeString; + + modeString += AccessMode(mode, file_exists); + + if (IsExtended(mode)) + modeString += '+'; + + if (!(mode & FileMode::Text)) + modeString += 'b'; + + return modeString; +} + +FileHandle* OpenFile(const std::string& path, FileMode mode) +{ + if ((mode & FileMode::ReadWrite) == FileMode::None) + { // If we aren't reading or writing, then we can't open the file + Log(LogLevel::Error, "Attempted to open \"%s\" in neither read nor write mode (FileMode 0x%x)\n", path.c_str(), mode); + return nullptr; + } + + bool file_exists = std::filesystem::exists(path); + std::string modeString = GetModeString(mode, file_exists); + + FILE* file = fopen(path.c_str(), modeString.c_str()); + if (file) + { + Log(LogLevel::Debug, "Opened \"%s\" with FileMode 0x%x (effective mode \"%s\")\n", path.c_str(), mode, modeString.c_str()); + return reinterpret_cast(file); + } + else + { + Log(LogLevel::Warn, "Failed to open \"%s\" with FileMode 0x%x (effective mode \"%s\")\n", path.c_str(), mode, modeString.c_str()); + return nullptr; + } +} + +FileHandle* OpenLocalFile(const std::string& path, FileMode mode) +{ + return OpenFile(path, mode); +} + +bool CloseFile(FileHandle* file) +{ + return fclose(reinterpret_cast(file)) == 0; +} + +bool IsEndOfFile(FileHandle* file) +{ + return feof(reinterpret_cast(file)) != 0; +} + +bool FileReadLine(char* str, int count, FileHandle* file) +{ + return fgets(str, count, reinterpret_cast(file)) != nullptr; +} + +bool FileExists(const std::string& name) +{ + FileHandle* f = OpenFile(name, FileMode::Read); + if (!f) return false; + CloseFile(f); + return true; +} + +bool LocalFileExists(const std::string& name) +{ + FileHandle* f = OpenLocalFile(name, FileMode::Read); + if (!f) return false; + CloseFile(f); + return true; +} + +bool FileSeek(FileHandle* file, s64 offset, FileSeekOrigin origin) +{ + int stdorigin; + switch (origin) + { + case FileSeekOrigin::Start: stdorigin = SEEK_SET; break; + case FileSeekOrigin::Current: stdorigin = SEEK_CUR; break; + case FileSeekOrigin::End: stdorigin = SEEK_END; break; + } + + return fseek(reinterpret_cast(file), offset, stdorigin) == 0; +} + +void FileRewind(FileHandle* file) +{ + rewind(reinterpret_cast(file)); +} + +u64 FileRead(void* data, u64 size, u64 count, FileHandle* file) +{ + return fread(data, size, count, reinterpret_cast(file)); +} + +bool FileFlush(FileHandle* file) +{ + return fflush(reinterpret_cast(file)) == 0; +} + +u64 FileWrite(const void* data, u64 size, u64 count, FileHandle* file) +{ + return fwrite(data, size, count, reinterpret_cast(file)); +} + +u64 FileWriteFormatted(FileHandle* file, const char* fmt, ...) +{ + if (fmt == nullptr) + return 0; + + va_list args; + va_start(args, fmt); + u64 ret = vfprintf(reinterpret_cast(file), fmt, args); + va_end(args); + return ret; +} + +u64 FileLength(FileHandle* file) +{ + FILE* stdfile = reinterpret_cast(file); + long pos = ftell(stdfile); + fseek(stdfile, 0, SEEK_END); + long len = ftell(stdfile); + fseek(stdfile, pos, SEEK_SET); + return len; +} + +void Log(LogLevel level, const char* fmt, ...) +{ + if (fmt == nullptr) + return; + + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); +} + +struct Thread +{ + std::thread thread; +}; + +Thread* Thread_Create(std::function func) +{ + return new Thread { std::thread(func) }; +} + +void Thread_Free(Thread* thread) +{ + if (thread) + { + thread->thread.join(); + delete thread; + } +} + +void Thread_Wait(Thread* thread) +{ + if (thread) + { + thread->thread.join(); + } +} + +struct Semaphore +{ + std::counting_semaphore<> semaphore; + + Semaphore() : semaphore(0) {} +}; + +Semaphore* Semaphore_Create() +{ + return new Semaphore; +} + +void Semaphore_Free(Semaphore* sema) +{ + delete sema; +} + +void Semaphore_Reset(Semaphore* sema) +{ + while (sema->semaphore.try_acquire()); +} + +void Semaphore_Wait(Semaphore* sema) +{ + sema->semaphore.acquire(); +} + +void Semaphore_Post(Semaphore* sema, int count) +{ + sema->semaphore.release(count); +} + +struct Mutex +{ + std::mutex mutex; +}; + +Mutex* Mutex_Create() +{ + return new Mutex; +} + +void Mutex_Free(Mutex* mutex) +{ + delete mutex; +} + +void Mutex_Lock(Mutex* mutex) +{ + if (mutex) + mutex->mutex.lock(); +} + +void Mutex_Unlock(Mutex* mutex) +{ + if (mutex) + mutex->mutex.unlock(); +} + +bool Mutex_TryLock(Mutex* mutex) +{ + if (!mutex) + return false; + + return mutex->mutex.try_lock(); +} + +void WriteNDSSave(const u8* savedata, u32 savelen, u32 writeoffset, u32 writelen) {} +void WriteGBASave(const u8* savedata, u32 savelen, u32 writeoffset, u32 writelen) {} +void WriteFirmware(const Firmware& firmware, u32 writeoffset, u32 writelen) {} +void WriteDateTime(int year, int month, int day, int hour, int minute, int second) {} +bool MP_Init() { return false; } +void MP_DeInit() {} +void MP_Begin() {} +void MP_End() {} +int MP_SendPacket(u8* data, int len, u64 timestamp) { return 0; } +int MP_RecvPacket(u8* data, u64* timestamp) { return 0;} +int MP_SendCmd(u8* data, int len, u64 timestamp) { return 0; } +int MP_SendReply(u8* data, int len, u64 timestamp, u16 aid) { return 0;} +int MP_SendAck(u8* data, int len, u64 timestamp) { return 0; } +int MP_RecvHostPacket(u8* data, u64* timestamp) { return 0; } +u16 MP_RecvReplies(u8* data, u64 timestamp, u16 aidmask) { return 0; } +bool LAN_Init() { return false; } +void LAN_DeInit() {} +int LAN_SendPacket(u8* data, int len) { return 0; } +int LAN_RecvPacket(u8* data) { return 0; } +void Camera_Start(int num) {} +void Camera_Stop(int num) {} +void Camera_CaptureFrame(int num, u32* frame, int width, int height, bool yuv) {} +DynamicLibrary* DynamicLibrary_Load(const char* lib) { return nullptr;} +void DynamicLibrary_Unload(DynamicLibrary* lib) {} +void* DynamicLibrary_LoadFunction(DynamicLibrary* lib, const char* name) { return nullptr; } +} \ No newline at end of file diff --git a/test/common/TestCommon.cpp b/test/common/TestCommon.cpp new file mode 100644 index 00000000..9bb7e982 --- /dev/null +++ b/test/common/TestCommon.cpp @@ -0,0 +1,23 @@ +/* + Copyright 2016-2023 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#include "TestCommon.h" + +namespace melonDS +{ +} diff --git a/test/common/TestCommon.h b/test/common/TestCommon.h new file mode 100644 index 00000000..01585ab2 --- /dev/null +++ b/test/common/TestCommon.h @@ -0,0 +1,25 @@ +/* + Copyright 2016-2023 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#ifndef MELONDS_TESTCOMMON_H +#define MELONDS_TESTCOMMON_H + +namespace melonDS +{ +} +#endif //MELONDS_TESTCOMMON_H