diff --git a/README.md b/README.md index 556543a..6cb5528 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # datacoe -datacoe is a small, simple and generic C++ data management library for game development, +datacoe is a small, simple and generic C++ data management library template for game development, It provides functionalities for data persistence, serialization, and encryption. [![Windows](https://github.com/nircoe/datacoe/actions/workflows/ci-windows.yml/badge.svg?branch=main&event=push)](https://github.com/nircoe/datacoe/actions/workflows/ci-windows.yml) @@ -286,7 +286,6 @@ The project includes a comprehensive test suite built with Google Test. Tests co - Basic data operations - Error handling and recovery -- Thread safety and concurrency - Performance benchmarks - Memory usage @@ -411,6 +410,13 @@ Game-specific implementations will have their own tags (e.g., `worm-v1.0.0`) to ## Version History +### v0.1.1 (Optional Encryption) +- Added ability to disable encryption when not needed +- Implemented automatic encryption detection for backwards compatibility +- Improved file handling with clear encryption status identification +- Enhanced performance for unencrypted files +- Added tests to measure and compare encryption overhead in terms of both time and file size + ### v0.1.0 (Initial Development) - Basic data management functionality - JSON serialization @@ -448,9 +454,9 @@ If you'd like to contribute, please: - ✅ AES encryption/decryption using CryptoPP - ✅ Comprehensive test suite with Google Test - ✅ Automated dependency management +- ✅ Optional encryption (ability to disable encryption if not needed) ### Planned Improvements -- ⏳ Optional encryption (ability to disable encryption if not needed) - ⏳ Secure encryption key management (replacing fixed keys with secure storage and derivation) - ⏳ Graceful recovery from corrupted files with backup system - ⏳ Thread-safe operations for concurrent data access diff --git a/include/data_manager.hpp b/include/data_manager.hpp index 161e8df..2cf206e 100644 --- a/include/data_manager.hpp +++ b/include/data_manager.hpp @@ -11,6 +11,8 @@ namespace datacoe { std::string m_filename; GameData m_gamedata; + bool m_encrypt = true; // Whether to use encryption + bool m_fileEncrypted = false; // Whether the file is currently encrypted public: // Users should add or modify constructors and destructor as needed @@ -22,7 +24,7 @@ namespace datacoe bool loadGame(); // Users should modify the initialization to match their own game - void init(const std::string filename); + void init(const std::string filename, bool encrypt = true); // Users should modify this method to match their own game void newGame(); @@ -32,5 +34,9 @@ namespace datacoe void setHighScore(int highscore); const GameData &getGameData() const; + + // Encryption related methods + bool isEncrypted() const; + void setEncryption(bool encrypt); }; } // namespace datacoe \ No newline at end of file diff --git a/include/data_reader_writer.hpp b/include/data_reader_writer.hpp index 22095ee..d5e590c 100644 --- a/include/data_reader_writer.hpp +++ b/include/data_reader_writer.hpp @@ -13,7 +13,8 @@ namespace datacoe static std::string decrypt(const std::string &encodedData); public: - static bool writeData(const GameData &gamedata, const std::string &filename); - static std::optional readData(const std::string &filename); + static bool isFileEncrypted(const std::string &filename); + static bool writeData(const GameData &gamedata, const std::string &filename, bool encryption = true); + static std::optional readData(const std::string &filename, bool decryption = true); }; } // namespace datacoe \ No newline at end of file diff --git a/src/data_manager.cpp b/src/data_manager.cpp index 87a8875..760e424 100644 --- a/src/data_manager.cpp +++ b/src/data_manager.cpp @@ -7,21 +7,29 @@ namespace datacoe if (m_gamedata.getNickName().empty()) return true; // no need to save (guest mode) - return DataReaderWriter::writeData(m_gamedata, m_filename); + bool result = DataReaderWriter::writeData(m_gamedata, m_filename, m_encrypt); + if (result) + m_fileEncrypted = m_encrypt; + + return result; } bool DataManager::loadGame() { - std::optional loadedGameData = DataReaderWriter::readData(m_filename); + m_fileEncrypted = DataReaderWriter::isFileEncrypted(m_filename); + + std::optional loadedGameData = DataReaderWriter::readData(m_filename, m_encrypt); bool readDataSucceed = loadedGameData.has_value(); if (readDataSucceed) m_gamedata = loadedGameData.value(); return readDataSucceed; } - void DataManager::init(const std::string filename) + void DataManager::init(const std::string filename, bool encrypt) { m_filename = filename; + m_encrypt = encrypt; + if (!loadGame()) { // can't load, needs to ask the user for a nickname and create new GameData @@ -48,4 +56,14 @@ namespace datacoe { return m_gamedata; } + + bool DataManager::isEncrypted() const + { + return m_fileEncrypted; + } + + void DataManager::setEncryption(bool encrypt) + { + m_encrypt = encrypt; + } } // namespace datacoe \ No newline at end of file diff --git a/src/data_reader_writer.cpp b/src/data_reader_writer.cpp index 3e8a95b..f1da4c0 100644 --- a/src/data_reader_writer.cpp +++ b/src/data_reader_writer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "cryptopp/aes.h" #include "cryptopp/modes.h" #include "cryptopp/filters.h" @@ -14,11 +15,34 @@ namespace datacoe { + const std::string ENCRYPTION_PREFIX = "DATACOE_ENCRYPTED"; + // Fixed Encryption Key (Warning: This is Insecure, I'm using it for learning purposes only!) const CryptoPP::byte fixedKey[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}; + bool DataReaderWriter::isFileEncrypted(const std::string &filename) + { + if (!std::filesystem::exists(filename)) + return false; + + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) + return false; + + // Read just enough bytes to check for our prefix + std::vector header(ENCRYPTION_PREFIX.size()); + file.read(header.data(), ENCRYPTION_PREFIX.size()); + + // Check if we read enough bytes + if (file.gcount() < static_cast(ENCRYPTION_PREFIX.size())) + return false; + + // Compare with our prefix + return std::string(header.data(), static_cast(file.gcount())) == ENCRYPTION_PREFIX; + } + std::string DataReaderWriter::encrypt(const std::string &data) { try @@ -45,7 +69,7 @@ namespace datacoe new CryptoPP::Base64Encoder( new CryptoPP::StringSink(encoded))); - return encoded; + return ENCRYPTION_PREFIX + encoded; } catch (const CryptoPP::Exception &e) { @@ -65,9 +89,20 @@ namespace datacoe { try { + std::string dataToDecrypt = encodedData; + if (dataToDecrypt.substr(0, ENCRYPTION_PREFIX.size()) == ENCRYPTION_PREFIX) + { + dataToDecrypt = dataToDecrypt.substr(ENCRYPTION_PREFIX.size()); + } + else + { + std::cerr << "DataReaderWriter::decrypt() Warning: Missing encryption prefix" << std::endl; + // Continue anyway in case it's an older file without the prefix + } + // Decode Base64 std::string decoded; - CryptoPP::StringSource ss1(encodedData, true, + CryptoPP::StringSource ss1(dataToDecrypt, true, new CryptoPP::Base64Decoder( new CryptoPP::StringSink(decoded))); @@ -106,7 +141,7 @@ namespace datacoe } } - bool DataReaderWriter::writeData(const GameData &gamedata, const std::string &filename) + bool DataReaderWriter::writeData(const GameData &gamedata, const std::string &filename, bool encryption) { try { @@ -115,23 +150,32 @@ namespace datacoe std::cout << "Debug: GameData to JSON: " << std::endl << jsonData << std::endl; - // Encrypt the JSON data - std::string encryptedData = encrypt(jsonData); - if (encryptedData.empty()) + std::string writeableData; + + if(encryption) { - std::cerr << "DataReaderWriter::writeData() Error: Encryption failed" << std::endl; - return false; + // Encrypt the JSON data + std::string encryptedData = encrypt(jsonData); + if (encryptedData.empty()) + { + std::cerr << "DataReaderWriter::writeData() Error: Encryption failed" << std::endl; + return false; + } + writeableData = encryptedData; } + else // no encryption + writeableData = jsonData; - // Write the encrypted data to file - std::ofstream file(filename, std::ios::binary); + // Write the data to file + auto openmode = encryption ? std::ios::binary : std::ios::out; + std::ofstream file(filename, openmode); if (!file.is_open()) { std::cerr << "DataReaderWriter::writeData() Error: Could not open file for writing: " << filename << std::endl; return false; } - file.write(encryptedData.c_str(), encryptedData.size()); + file.write(writeableData.c_str(), writeableData.size()); if (!file.good()) { std::cerr << "DataReaderWriter::writeData() Error: File write failed" << std::endl; @@ -150,7 +194,7 @@ namespace datacoe } } - std::optional DataReaderWriter::readData(const std::string &filename) + std::optional DataReaderWriter::readData(const std::string &filename, bool decryption) { try { @@ -160,30 +204,49 @@ namespace datacoe return std::nullopt; } - // Read the encrypted data from file - std::ifstream file(filename, std::ios::binary); + bool fileIsEncrypted = isFileEncrypted(filename); + if(fileIsEncrypted != decryption) + { + std::cerr << "DataReaderWriter::readData() Warning: " + << (fileIsEncrypted ? "File is encrypted but decryption=false" + : "File is not encrypted but decryption=true") + << " - Adjusting decryption flag to match file state" << std::endl; + decryption = fileIsEncrypted; + } + + // Read the data from file + auto openmode = decryption ? std::ios::binary : std::ios::in; + std::ifstream file(filename, openmode); if (!file.is_open()) { std::cerr << "DataReaderWriter::readData() Error: Could not open file for reading: " << filename << std::endl; return std::nullopt; } - std::string encodedData((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + std::string data((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); - // Decrypt the data - std::string decryptedData = decrypt(encodedData); - if (decryptedData.empty()) + std::string parseableData; + + if(decryption) { - std::cerr << "DataReaderWriter::readData() Error: Decryption failed" << std::endl; - return std::nullopt; - } + // Decrypt the data + std::string decryptedData = decrypt(data); + if (decryptedData.empty()) + { + std::cerr << "DataReaderWriter::readData() Error: Decryption failed" << std::endl; + return std::nullopt; + } - std::cout << "Debug: Decrypted JSON: " << std::endl - << decryptedData << std::endl; + std::cout << "Debug: Decrypted JSON: " << std::endl + << decryptedData << std::endl; + parseableData = decryptedData; + } + else // no decryption + parseableData = data; // Parse the JSON data - json j = json::parse(decryptedData); + json j = json::parse(parseableData); return GameData::fromJson(j); } catch (const json::exception &e) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2ddbffd..6b87151 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,7 +15,6 @@ set(TEST_FILES integration_tests.cpp performance_tests.cpp memory_tests.cpp - thread_safety_tests.cpp error_handling_tests.cpp ) diff --git a/tests/data_manager_tests.cpp b/tests/data_manager_tests.cpp index b7c2495..7a1af0e 100644 --- a/tests/data_manager_tests.cpp +++ b/tests/data_manager_tests.cpp @@ -8,19 +8,20 @@ namespace datacoe { - class DataManagerTest : public ::testing::Test { protected: + std::string m_testFilename; + void SetUp() override { - testFilename = "test_data_manager.json"; + m_testFilename = "test_data_manager.json"; // Clean up any leftover files from previous tests try { - if (std::filesystem::exists(testFilename)) + if (std::filesystem::exists(m_testFilename)) { - std::filesystem::remove(testFilename); + std::filesystem::remove(m_testFilename); } } catch (const std::filesystem::filesystem_error &) @@ -39,9 +40,9 @@ namespace datacoe { try { - if (std::filesystem::exists(testFilename)) + if (std::filesystem::exists(m_testFilename)) { - std::filesystem::remove(testFilename); + std::filesystem::remove(m_testFilename); } break; } @@ -52,8 +53,6 @@ namespace datacoe } } } - - std::string testFilename; }; TEST_F(DataManagerTest, SaveAndLoadGame) @@ -61,17 +60,17 @@ namespace datacoe try { DataManager dm; - dm.init(testFilename); + dm.init(m_testFilename); dm.setNickName("TestUser"); dm.setHighScore(100); ASSERT_TRUE(dm.saveGame()) << "Failed to save game"; - ASSERT_TRUE(std::filesystem::exists(testFilename)) << "Save file not created"; + ASSERT_TRUE(std::filesystem::exists(m_testFilename)) << "Save file not created"; // Create a new DataManager to load the saved data DataManager dm2; - dm2.init(testFilename); + dm2.init(m_testFilename); const GameData &loadedData = dm2.getGameData(); @@ -89,7 +88,7 @@ namespace datacoe try { DataManager dm; - dm.init(testFilename); + dm.init(m_testFilename); // Set initial data and save dm.setNickName("TestUser"); @@ -102,7 +101,7 @@ namespace datacoe // Load in a new manager DataManager dm2; - dm2.init(testFilename); + dm2.init(m_testFilename); const GameData &loadedData = dm2.getGameData(); @@ -121,7 +120,7 @@ namespace datacoe try { DataManager dm; - dm.init(testFilename); + dm.init(m_testFilename); dm.setNickName("TestUser"); dm.setHighScore(100); dm.saveGame(); @@ -137,7 +136,7 @@ namespace datacoe // Check that original saved data still exists on disk DataManager dm2; - dm2.init(testFilename); + dm2.init(m_testFilename); const GameData &loadedData = dm2.getGameData(); ASSERT_EQ(loadedData.getNickName(), "TestUser"); @@ -172,7 +171,7 @@ namespace datacoe try { DataManager dm; - dm.init(testFilename); + dm.init(m_testFilename); // Empty nickname represents guest mode dm.setNickName(""); @@ -180,7 +179,7 @@ namespace datacoe // Should return true but not create a file ASSERT_TRUE(dm.saveGame()); - ASSERT_FALSE(std::filesystem::exists(testFilename)) << "File should not be created for guest mode"; + ASSERT_FALSE(std::filesystem::exists(m_testFilename)) << "File should not be created for guest mode"; } catch (const std::exception &e) { @@ -193,7 +192,7 @@ namespace datacoe try { DataManager dm; - dm.init(testFilename); + dm.init(m_testFilename); dm.setNickName("Player1"); dm.setHighScore(100); @@ -205,7 +204,7 @@ namespace datacoe // Load data and verify the lower score was saved DataManager dm2; - dm2.init(testFilename); + dm2.init(m_testFilename); ASSERT_EQ(dm2.getGameData().getHighscore(), 50); // Set a higher score @@ -214,7 +213,7 @@ namespace datacoe // Load again and verify DataManager dm3; - dm3.init(testFilename); + dm3.init(m_testFilename); ASSERT_EQ(dm3.getGameData().getHighscore(), 200); } catch (const std::exception &e) @@ -223,4 +222,204 @@ namespace datacoe } } + TEST_F(DataManagerTest, EncryptionTransition) + { + try + { + // Create an unencrypted save file + { + DataManager dm; + dm.init(m_testFilename, false); // Unencrypted + dm.setNickName("TransitionTest"); + dm.setHighScore(1000); + ASSERT_TRUE(dm.saveGame()) << "Failed to save unencrypted game"; + + // Verify it's unencrypted + ASSERT_FALSE(DataReaderWriter::isFileEncrypted(m_testFilename)); + } + + // Load the file with encryption turned on + { + DataManager dm; + dm.init(m_testFilename, true); // Encrypted mode + + // Should still load successfully due to auto-detection + const GameData &data = dm.getGameData(); + ASSERT_EQ(data.getNickName(), "TransitionTest"); + ASSERT_EQ(data.getHighscore(), 1000); + + // Modify data + dm.setHighScore(2000); + + // Save with encryption turned on + ASSERT_TRUE(dm.saveGame()) << "Failed to save with encryption"; + + // Verify file is now encrypted + ASSERT_TRUE(DataReaderWriter::isFileEncrypted(m_testFilename)); + } + + // Load the now-encrypted file with encryption turned off + { + DataManager dm; + dm.init(m_testFilename, false); // Unencrypted mode + + // Should still load successfully due to auto-detection + const GameData &data = dm.getGameData(); + ASSERT_EQ(data.getNickName(), "TransitionTest"); + ASSERT_EQ(data.getHighscore(), 2000); + } + } + catch (const std::exception &e) + { + FAIL() << "Unexpected exception: " << e.what(); + } + } + + TEST_F(DataManagerTest, SetEncryptionDuringOperation) + { + try + { + // Create an unencrypted save file + { + DataManager dm; + dm.init(m_testFilename, false); // Unencrypted + dm.setNickName("EncryptionChangeTest"); + dm.setHighScore(100); + ASSERT_TRUE(dm.saveGame()) << "Failed to save unencrypted game"; + + // Verify it's unencrypted + ASSERT_FALSE(DataReaderWriter::isFileEncrypted(m_testFilename)); + + // Change encryption setting mid-operation + dm.setEncryption(true); + + // Update data + dm.setHighScore(200); + + // Save with new encryption setting + ASSERT_TRUE(dm.saveGame()) << "Failed to save with changed encryption"; + + // Verify file is now encrypted + ASSERT_TRUE(DataReaderWriter::isFileEncrypted(m_testFilename)); + } + + // Load the now-encrypted file with correct setting + { + DataManager dm; + dm.init(m_testFilename, true); // Encrypted mode + + // Should load successfully + const GameData &data = dm.getGameData(); + ASSERT_EQ(data.getNickName(), "EncryptionChangeTest"); + ASSERT_EQ(data.getHighscore(), 200); + } + } + catch (const std::exception &e) + { + FAIL() << "Unexpected exception: " << e.what(); + } + } + + TEST_F(DataManagerTest, IsEncryptedMethod) + { + try + { + // Test initial state after construction - file doesn't exist yet + { + DataManager dm; + // isEncrypted should return m_fileEncrypted, which should be false initially + ASSERT_FALSE(dm.isEncrypted()) << "File encryption state should initially be false"; + } + + // Create an encrypted file and test if isEncrypted returns true + { + // Create and save an encrypted file + DataManager dm; + dm.init(m_testFilename, true); // Use encryption + dm.setNickName("EncryptedTest"); + dm.setHighScore(100); + ASSERT_TRUE(dm.saveGame()); + + // Now verify the file is encrypted and isEncrypted() returns true + ASSERT_TRUE(DataReaderWriter::isFileEncrypted(m_testFilename)); + ASSERT_TRUE(dm.isEncrypted()) << "isEncrypted should return true after saving encrypted file"; + } + + // Create an unencrypted file and test if isEncrypted returns false + { + // Clear existing file to force creating a new one + std::filesystem::remove(m_testFilename); + + // Create unencrypted file + DataManager dm; + dm.init(m_testFilename, false); // No encryption + dm.setNickName("UnencryptedTest"); + dm.setHighScore(200); + ASSERT_TRUE(dm.saveGame()); + + // Now verify the file is not encrypted and isEncrypted() returns false + ASSERT_FALSE(DataReaderWriter::isFileEncrypted(m_testFilename)); + ASSERT_FALSE(dm.isEncrypted()) << "isEncrypted should return false after saving unencrypted file"; + } + + // Test loading an encrypted file - isEncrypted should return true + { + // First create an encrypted file + { + // Clear existing file + std::filesystem::remove(m_testFilename); + + DataManager dm; + dm.init(m_testFilename, true); // Encryption on + dm.setNickName("EncryptionStateTest"); + dm.setHighScore(300); + ASSERT_TRUE(dm.saveGame()); + } + + // Now load it with a new DataManager and check isEncrypted + { + DataManager dm; + dm.init(m_testFilename, true); // Match file encryption + ASSERT_TRUE(dm.isEncrypted()) << "isEncrypted should reflect the file's encryption state after loading"; + } + } + + // Test changing encryption with setEncryption and saving + { + // Set up initial state - encrypted file + { + // Clear existing file + std::filesystem::remove(m_testFilename); + + DataManager dm; + dm.init(m_testFilename, true); // Start with encryption + dm.setNickName("ChangeEncryptionTest"); + dm.setHighScore(300); + ASSERT_TRUE(dm.saveGame()); + } + + // Load file, change encryption setting, and save + { + DataManager dm; + dm.init(m_testFilename, true); // Load encrypted file + + // Initial state should be encrypted + ASSERT_TRUE(dm.isEncrypted()); + + // Change encryption setting and save + dm.setEncryption(false); + dm.setHighScore(400); // Change data + ASSERT_TRUE(dm.saveGame()); // Save with new encryption setting + + // After saving, isEncrypted should reflect the new state + ASSERT_FALSE(dm.isEncrypted()) << "isEncrypted should return false after saving with encryption off"; + ASSERT_FALSE(DataReaderWriter::isFileEncrypted(m_testFilename)); + } + } + } + catch (const std::exception &e) + { + FAIL() << "Unexpected exception: " << e.what(); + } + } } // namespace datacoe \ No newline at end of file diff --git a/tests/data_reader_writer_tests.cpp b/tests/data_reader_writer_tests.cpp index 7e76b4a..3bf60f9 100644 --- a/tests/data_reader_writer_tests.cpp +++ b/tests/data_reader_writer_tests.cpp @@ -8,10 +8,11 @@ namespace datacoe { - class DataReaderWriterTest : public ::testing::Test { protected: + std::string m_testFilename; + void SetUp() override { m_testFilename = "test_data_rw.data"; @@ -52,8 +53,6 @@ namespace datacoe } } } - - std::string m_testFilename; }; TEST_F(DataReaderWriterTest, WriteAndReadData) @@ -132,4 +131,167 @@ namespace datacoe ASSERT_FALSE(loadedData.has_value()) << "Expected failure on corrupted file"; } + TEST_F(DataReaderWriterTest, WriteAndReadDataWithAutoDetection) + { + std::string unencryptedFilename = m_testFilename + ".unencrypted"; + + try + { + // Create test data + GameData originalData("AutoDetectTest", 300); + + // Write with encryption + bool writeEncryptedResult = DataReaderWriter::writeData(originalData, m_testFilename, true); + ASSERT_TRUE(writeEncryptedResult) << "Failed to write encrypted data"; + ASSERT_TRUE(std::filesystem::exists(m_testFilename)) << "File was not created"; + ASSERT_TRUE(DataReaderWriter::isFileEncrypted(m_testFilename)) << "File should be detected as encrypted"; + + // Try to read with decryption=false + // Should auto-adjust to decryption=true based on file detection + std::optional loadedEncrypted = DataReaderWriter::readData(m_testFilename, false); + ASSERT_TRUE(loadedEncrypted.has_value()) << "Failed to read encrypted data with auto-detection"; + + // Verify data was read correctly + ASSERT_EQ(loadedEncrypted.value().getNickName(), "AutoDetectTest"); + ASSERT_EQ(loadedEncrypted.value().getHighscore(), 300); + + // Now write the same data unencrypted to a new file + bool writeUnencryptedResult = DataReaderWriter::writeData(originalData, unencryptedFilename, false); + ASSERT_TRUE(writeUnencryptedResult) << "Failed to write unencrypted data"; + ASSERT_FALSE(DataReaderWriter::isFileEncrypted(unencryptedFilename)) << "File should be detected as unencrypted"; + + // Try to read with decryption=true + // Should auto-adjust to decryption=false based on file detection + std::optional loadedUnencrypted = DataReaderWriter::readData(unencryptedFilename, true); + ASSERT_TRUE(loadedUnencrypted.has_value()) << "Failed to read unencrypted data with auto-detection"; + + // Verify data was read correctly + ASSERT_EQ(loadedUnencrypted.value().getNickName(), "AutoDetectTest"); + ASSERT_EQ(loadedUnencrypted.value().getHighscore(), 300); + } + catch (const std::exception &e) + { + // Clean up in exception path + if (std::filesystem::exists(unencryptedFilename)) + { + try + { + std::filesystem::remove(unencryptedFilename); + } + catch (...) { /* Ignore cleanup errors */ } + } + FAIL() << "Unexpected exception: " << e.what(); + } + + // Clean up in normal path + if (std::filesystem::exists(unencryptedFilename)) + { + try + { + std::filesystem::remove(unencryptedFilename); + } + catch (...) { /* Ignore cleanup errors */ } + } + } + + TEST_F(DataReaderWriterTest, EncryptionDetection) + { + std::string unencryptedFilename = m_testFilename + ".unencrypted"; + std::string invalidFilename = m_testFilename + ".invalid"; + + try + { + // Create test data + GameData testData("EncryptionDetectionTest", 400); + + // Write encrypted data + ASSERT_TRUE(DataReaderWriter::writeData(testData, m_testFilename, true)); + ASSERT_TRUE(DataReaderWriter::isFileEncrypted(m_testFilename)) << "File should be detected as encrypted"; + + // Write unencrypted data to a different file + ASSERT_TRUE(DataReaderWriter::writeData(testData, unencryptedFilename, false)); + ASSERT_FALSE(DataReaderWriter::isFileEncrypted(unencryptedFilename)) << "File should be detected as unencrypted"; + + // Create a file with invalid data + { + std::ofstream file(invalidFilename); + file << "This is not a valid encrypted or JSON file"; + file.close(); + } + ASSERT_FALSE(DataReaderWriter::isFileEncrypted(invalidFilename)) << "Invalid file should not be detected as encrypted"; + + // Test with non-existent file + ASSERT_FALSE(DataReaderWriter::isFileEncrypted("non_existent_file.json")) << "Non-existent file should not be detected as encrypted"; + } + catch (const std::exception &e) + { + // Clean up in exception path + try + { + if (std::filesystem::exists(unencryptedFilename)) + std::filesystem::remove(unencryptedFilename); + if (std::filesystem::exists(invalidFilename)) + std::filesystem::remove(invalidFilename); + } + catch (...) { /* Ignore cleanup errors */ } + + FAIL() << "Unexpected exception: " << e.what(); + } + + // Clean up in normal path + try + { + if (std::filesystem::exists(unencryptedFilename)) + std::filesystem::remove(unencryptedFilename); + if (std::filesystem::exists(invalidFilename)) + std::filesystem::remove(invalidFilename); + } + catch (...) { /* Ignore cleanup errors */ } + } + + TEST_F(DataReaderWriterTest, WriteEncryptedReadUnencrypted) + { + try + { + // Create and write encrypted data + GameData originalData("EncryptedData", 500); + ASSERT_TRUE(DataReaderWriter::writeData(originalData, m_testFilename, true)); + + // Try to read without decryption + // This should auto-adjust and work correctly + std::optional loadedData = DataReaderWriter::readData(m_testFilename, false); + ASSERT_TRUE(loadedData.has_value()) << "Should be able to read encrypted data with auto-detection"; + + // Verify data + ASSERT_EQ(loadedData.value().getNickName(), "EncryptedData"); + ASSERT_EQ(loadedData.value().getHighscore(), 500); + } + catch (const std::exception &e) + { + FAIL() << "Unexpected exception: " << e.what(); + } + } + + TEST_F(DataReaderWriterTest, WriteUnencryptedReadEncrypted) + { + try + { + // Create and write unencrypted data + GameData originalData("UnencryptedData", 600); + ASSERT_TRUE(DataReaderWriter::writeData(originalData, m_testFilename, false)); + + // Try to read with decryption + // This should auto-adjust and work correctly + std::optional loadedData = DataReaderWriter::readData(m_testFilename, true); + ASSERT_TRUE(loadedData.has_value()) << "Should be able to read unencrypted data with auto-detection"; + + // Verify data + ASSERT_EQ(loadedData.value().getNickName(), "UnencryptedData"); + ASSERT_EQ(loadedData.value().getHighscore(), 600); + } + catch (const std::exception &e) + { + FAIL() << "Unexpected exception: " << e.what(); + } + } } // namespace datacoe \ No newline at end of file diff --git a/tests/error_handling_tests.cpp b/tests/error_handling_tests.cpp index 77ff1e6..4fcd3c9 100644 --- a/tests/error_handling_tests.cpp +++ b/tests/error_handling_tests.cpp @@ -14,10 +14,12 @@ namespace datacoe { - class ErrorHandlingTest : public ::testing::Test { protected: + std::string m_testFilename; + std::string m_corruptFilename; + void SetUp() override { m_testFilename = "error_test_data.json"; @@ -72,9 +74,6 @@ namespace datacoe } } - std::string m_testFilename; - std::string m_corruptFilename; - // Helper to create a corrupted file void createCorruptJsonFile() { diff --git a/tests/game_data_tests.cpp b/tests/game_data_tests.cpp index 63a9c3d..4bba22d 100644 --- a/tests/game_data_tests.cpp +++ b/tests/game_data_tests.cpp @@ -4,7 +4,6 @@ namespace datacoe { - TEST(GameDataTest, DefaultConstructor) { GameData gd; diff --git a/tests/integration_tests.cpp b/tests/integration_tests.cpp index 3064469..b26043f 100644 --- a/tests/integration_tests.cpp +++ b/tests/integration_tests.cpp @@ -8,10 +8,11 @@ namespace datacoe { - class IntegrationTest : public ::testing::Test { protected: + std::string m_testFilename; + void SetUp() override { m_testFilename = "test_integration.json"; @@ -52,8 +53,6 @@ namespace datacoe } } } - - std::string m_testFilename; }; TEST_F(IntegrationTest, FullLifecycle) diff --git a/tests/memory_tests.cpp b/tests/memory_tests.cpp index 7fab07c..c739f5c 100644 --- a/tests/memory_tests.cpp +++ b/tests/memory_tests.cpp @@ -7,10 +7,11 @@ namespace datacoe { - class MemoryTest : public ::testing::Test { protected: + std::string m_testFilename; + void SetUp() override { m_testFilename = "memory_test_data.json"; @@ -42,8 +43,6 @@ namespace datacoe // Ignore errors } } - - std::string m_testFilename; }; TEST_F(MemoryTest, RepeatedCreationAndDestruction) diff --git a/tests/performance_tests.cpp b/tests/performance_tests.cpp index 4f8fe14..08757de 100644 --- a/tests/performance_tests.cpp +++ b/tests/performance_tests.cpp @@ -10,10 +10,11 @@ namespace datacoe { - class PerformanceTest : public ::testing::Test { protected: + std::string m_testFilename; + void SetUp() override { m_testFilename = "perf_test_data.json"; @@ -54,8 +55,6 @@ namespace datacoe auto end = std::chrono::high_resolution_clock::now(); return std::chrono::duration_cast(end - start).count(); } - - std::string m_testFilename; }; TEST_F(PerformanceTest, SavePerformance) @@ -182,17 +181,17 @@ namespace datacoe switch (operation) { - case 0: // Save - dm.setNickName(names[nameDist(gen)]); - dm.setHighScore(scoreDist(gen)); - dm.saveGame(); - break; - case 1: // Load - dm.loadGame(); - break; - case 2: // New game - dm.newGame(); - break; + case 0: // Save + dm.setNickName(names[nameDist(gen)]); + dm.setHighScore(scoreDist(gen)); + dm.saveGame(); + break; + case 1: // Load + dm.loadGame(); + break; + case 2: // New game + dm.newGame(); + break; } } @@ -214,4 +213,149 @@ namespace datacoe ASSERT_EQ(dm2.getGameData().getHighscore(), 12345); } + TEST_F(PerformanceTest, EncryptionPerformanceComparison) + { + constexpr int iterations = 50; + + // Setup test data + GameData testData("PerformanceTest", 12345); + std::string encryptedFilename = m_testFilename + ".encrypted"; + std::string unencryptedFilename = m_testFilename + ".unencrypted"; + + std::vector encryptedSaveTimings; + std::vector unencryptedSaveTimings; + std::vector encryptedLoadTimings; + std::vector unencryptedLoadTimings; + + try + { + encryptedSaveTimings.reserve(iterations); + unencryptedSaveTimings.reserve(iterations); + encryptedLoadTimings.reserve(iterations); + unencryptedLoadTimings.reserve(iterations); + + // Create initial files + ASSERT_TRUE(DataReaderWriter::writeData(testData, encryptedFilename, true)); + ASSERT_TRUE(DataReaderWriter::writeData(testData, unencryptedFilename, false)); + + // Measure save performance + for (int i = 0; i < iterations; i++) + { + // Modify data slightly to avoid caching effects + testData.setHighScore(12345 + i); + + // Measure encrypted save + auto encryptedSaveTime = measureExecutionTime([&]() + { DataReaderWriter::writeData(testData, encryptedFilename, true); }); + encryptedSaveTimings.push_back(encryptedSaveTime); + + // Measure unencrypted save + auto unencryptedSaveTime = measureExecutionTime([&]() + { DataReaderWriter::writeData(testData, unencryptedFilename, false); }); + unencryptedSaveTimings.push_back(unencryptedSaveTime); + } + + // Measure load performance + for (int i = 0; i < iterations; i++) + { + // Measure encrypted load + auto encryptedLoadTime = measureExecutionTime([&]() + { auto data = DataReaderWriter::readData(encryptedFilename, true); }); + encryptedLoadTimings.push_back(encryptedLoadTime); + + // Measure unencrypted load + auto unencryptedLoadTime = measureExecutionTime([&]() + { auto data = DataReaderWriter::readData(unencryptedFilename, false); }); + unencryptedLoadTimings.push_back(unencryptedLoadTime); + } + + // Calculate statistics for encrypted save + double encSaveAvg = 0.0; + for (auto time : encryptedSaveTimings) + { + encSaveAvg += time; + } + encSaveAvg /= iterations; + + // Calculate statistics for unencrypted save + double unencSaveAvg = 0.0; + for (auto time : unencryptedSaveTimings) + { + unencSaveAvg += time; + } + unencSaveAvg /= iterations; + + // Calculate statistics for encrypted load + double encLoadAvg = 0.0; + for (auto time : encryptedLoadTimings) + { + encLoadAvg += time; + } + encLoadAvg /= iterations; + + // Calculate statistics for unencrypted load + double unencLoadAvg = 0.0; + for (auto time : unencryptedLoadTimings) + { + unencLoadAvg += time; + } + unencLoadAvg /= iterations; + + // Calculate performance impact percentages + double saveImpact = ((encSaveAvg / unencSaveAvg) - 1.0) * 100.0; + double loadImpact = ((encLoadAvg / unencLoadAvg) - 1.0) * 100.0; + + // Output results + std::cout << "=============================================" << std::endl; + std::cout << " Encryption Performance Comparison" << std::endl; + std::cout << "=============================================" << std::endl; + std::cout << std::fixed << std::setprecision(2); + std::cout << "Save operations (microseconds):" << std::endl; + std::cout << " Encrypted average: " << encSaveAvg << std::endl; + std::cout << " Unencrypted average: " << unencSaveAvg << std::endl; + std::cout << " Encryption overhead: " << (saveImpact > 0 ? "+" : "") << saveImpact << "%" << std::endl; + std::cout << std::endl; + + std::cout << "Load operations (microseconds):" << std::endl; + std::cout << " Encrypted average: " << encLoadAvg << std::endl; + std::cout << " Unencrypted average: " << unencLoadAvg << std::endl; + std::cout << " Encryption overhead: " << (loadImpact > 0 ? "+" : "") << loadImpact << "%" << std::endl; + std::cout << "=============================================" << std::endl; + + // Also measure file size difference + std::uintmax_t encryptedSize = std::filesystem::file_size(encryptedFilename); + std::uintmax_t unencryptedSize = std::filesystem::file_size(unencryptedFilename); + double sizeImpact = ((double)encryptedSize / unencryptedSize - 1.0) * 100.0; + + std::cout << "File size comparison:" << std::endl; + std::cout << " Encrypted: " << encryptedSize << " bytes" << std::endl; + std::cout << " Unencrypted: " << unencryptedSize << " bytes" << std::endl; + std::cout << " Size overhead: " << (sizeImpact > 0 ? "+" : "") << sizeImpact << "%" << std::endl; + std::cout << "=============================================" << std::endl; + } + catch (const std::exception &e) + { + // Clean up in exception path + try + { + if (std::filesystem::exists(encryptedFilename)) + std::filesystem::remove(encryptedFilename); + if (std::filesystem::exists(unencryptedFilename)) + std::filesystem::remove(unencryptedFilename); + } + catch (...) { /* Ignore cleanup errors */ } + + FAIL() << "Unexpected exception: " << e.what(); + } + + // Clean up in normal path + try + { + if (std::filesystem::exists(encryptedFilename)) + std::filesystem::remove(encryptedFilename); + if (std::filesystem::exists(unencryptedFilename)) + std::filesystem::remove(unencryptedFilename); + } + catch (...) { /* Ignore cleanup errors */ } + } } // namespace datacoe \ No newline at end of file diff --git a/tests/tester.cpp b/tests/tester.cpp index 6cb3fe0..dce6526 100644 --- a/tests/tester.cpp +++ b/tests/tester.cpp @@ -9,21 +9,29 @@ #include #include +// Optional includes for stack trace, conditionally included based on platform +#ifdef __GLIBC__ +#include +#include +#endif + // ANSI color codes for terminal output -namespace Color { - const std::string Reset = "\033[0m"; - const std::string Red = "\033[31m"; - const std::string Green = "\033[32m"; - const std::string Yellow = "\033[33m"; - const std::string Blue = "\033[34m"; - const std::string Magenta = "\033[35m"; - const std::string Cyan = "\033[36m"; - const std::string White = "\033[37m"; - const std::string Bold = "\033[1m"; -} +namespace color +{ + const std::string reset = "\033[0m"; + const std::string red = "\033[31m"; + const std::string green = "\033[32m"; + const std::string yellow = "\033[33m"; + const std::string blue = "\033[34m"; + const std::string magenta = "\033[35m"; + const std::string cyan = "\033[36m"; + const std::string white = "\033[37m"; + const std::string bold = "\033[1m"; +} // namespace color // Test status representation -enum class TestStatus { +enum class TestStatus +{ NotRun, Running, Passed, @@ -38,14 +46,15 @@ const char FAILED_CHAR = 'F'; const char EMPTY_CHAR = '\0'; // Global variables for original stream buffers -std::streambuf* g_origCoutBuf = nullptr; -std::streambuf* g_origCerrBuf = nullptr; +std::streambuf *g_origCoutBuf = nullptr; +std::streambuf *g_origCerrBuf = nullptr; // Custom listener for displaying test progress as a grid -class GridTestListener : public testing::TestEventListener { +class GridTestListener : public testing::TestEventListener +{ private: - testing::TestEventListener* m_originalListener; - + testing::TestEventListener *m_originalListener; + // Data for tracking test progress std::map> m_suiteTestStatus; std::map> m_suiteTestNames; @@ -59,282 +68,334 @@ class GridTestListener : public testing::TestEventListener { int m_passedTests; int m_failedTests; std::chrono::time_point m_startTime; - + // For stream redirection - std::streambuf* m_origCoutBuf; - std::streambuf* m_origCerrBuf; + std::streambuf *m_origCoutBuf; + std::streambuf *m_origCerrBuf; std::stringstream m_nullStream; public: - GridTestListener(testing::TestEventListener* listener) + GridTestListener(testing::TestEventListener *listener) : m_originalListener(listener), m_totalTests(0), m_completedTests(0), m_passedTests(0), m_failedTests(0), - m_origCoutBuf(std::cout.rdbuf()), m_origCerrBuf(std::cerr.rdbuf()) { + m_origCoutBuf(std::cout.rdbuf()), m_origCerrBuf(std::cerr.rdbuf()) + { m_startTime = std::chrono::high_resolution_clock::now(); // Save global copies for signal handler g_origCoutBuf = m_origCoutBuf; g_origCerrBuf = m_origCerrBuf; - } - ~GridTestListener() override { + ~GridTestListener() override + { // Ensure we restore the original buffers std::cout.rdbuf(m_origCoutBuf); std::cerr.rdbuf(m_origCerrBuf); - delete m_originalListener; + delete m_originalListener; } - void OnTestProgramStart(const testing::UnitTest& unitTest) override { + void OnTestProgramStart(const testing::UnitTest &unitTest) override + { // Redirect stdout/stderr to our null stream during test execution std::cout.rdbuf(m_origCoutBuf); // Temporarily restore for our output - + // First, gather all test suites and test cases m_totalTests = unitTest.total_test_count(); - for (int i = 0; i < unitTest.total_test_suite_count(); ++i) { - const testing::TestSuite* testSuite = unitTest.GetTestSuite(i); + for (int i = 0; i < unitTest.total_test_suite_count(); ++i) + { + const testing::TestSuite *testSuite = unitTest.GetTestSuite(i); std::string suiteName = testSuite->name(); - + // Initialize vectors for this test suite m_suiteTestStatus[suiteName] = std::vector(testSuite->total_test_count(), TestStatus::NotRun); m_suiteTestNames[suiteName] = std::vector(testSuite->total_test_count()); m_currentTestIndex[suiteName] = 0; - + // Save test names - for (int j = 0; j < testSuite->total_test_count(); ++j) { - const testing::TestInfo* testInfo = testSuite->GetTestInfo(j); + for (int j = 0; j < testSuite->total_test_count(); ++j) + { + const testing::TestInfo *testInfo = testSuite->GetTestInfo(j); m_suiteTestNames[suiteName][j] = testInfo->name(); } } - + // Display header - std::cout << Color::Bold << "Running " << m_totalTests << " tests...\n" << Color::Reset; + std::cout << color::bold << "Running " << m_totalTests << " tests...\n" + << color::reset; printGrid(); - + // Now redirect output std::cout.rdbuf(m_nullStream.rdbuf()); std::cerr.rdbuf(m_nullStream.rdbuf()); } - void OnTestIterationStart(const testing::UnitTest& /*unitTest*/, int /*iteration*/) override { - // Silent - } + void OnTestIterationStart(const testing::UnitTest & /*unitTest*/, int /*iteration*/) override { /* Silent */ } - void OnEnvironmentsSetUpStart(const testing::UnitTest& /*unitTest*/) override { - // Silent - } + void OnEnvironmentsSetUpStart(const testing::UnitTest & /*unitTest*/) override { /* Silent */ } - void OnEnvironmentsSetUpEnd(const testing::UnitTest& /*unitTest*/) override { - // Silent - } + void OnEnvironmentsSetUpEnd(const testing::UnitTest & /*unitTest*/) override { /* Silent */ } - void OnTestSuiteStart(const testing::TestSuite& testSuite) override { + void OnTestSuiteStart(const testing::TestSuite &testSuite) override + { m_currentTestSuite = testSuite.name(); } - void OnTestStart(const testing::TestInfo& testInfo) override { + void OnTestStart(const testing::TestInfo &testInfo) override + { m_currentTest = testInfo.name(); m_currentOutput.str(""); m_currentOutput.clear(); - + // Mark test as running int testIndex = m_currentTestIndex[m_currentTestSuite]; m_suiteTestStatus[m_currentTestSuite][testIndex] = TestStatus::Running; - + // Update the grid std::cout.rdbuf(m_origCoutBuf); printGrid(); std::cout.rdbuf(m_nullStream.rdbuf()); } - void OnTestPartResult(const testing::TestPartResult& testPartResult) override { - if (testPartResult.failed()) { - m_currentOutput << testPartResult.file_name() << ":" + void OnTestPartResult(const testing::TestPartResult &testPartResult) override + { + if (testPartResult.failed()) + { + m_currentOutput << testPartResult.file_name() << ":" << testPartResult.line_number() << ": Failure\n" << testPartResult.summary() << "\n"; } } - void OnTestEnd(const testing::TestInfo& testInfo) override { + void OnTestEnd(const testing::TestInfo &testInfo) override + { int testIndex = m_currentTestIndex[m_currentTestSuite]++; - + // Update status based on test result - if (testInfo.result()->Failed()) { + if (testInfo.result()->Failed()) + { m_suiteTestStatus[m_currentTestSuite][testIndex] = TestStatus::Failed; m_failedTests++; - + // Store the output for failed tests m_failedTestOutput[m_currentTestSuite].push_back( - m_currentTest + ":\n" + m_currentOutput.str() - ); - } else { + m_currentTest + ":\n" + m_currentOutput.str()); + } + else + { m_suiteTestStatus[m_currentTestSuite][testIndex] = TestStatus::Passed; m_passedTests++; } - + m_completedTests++; - + // Update the grid std::cout.rdbuf(m_origCoutBuf); printGrid(); std::cout.rdbuf(m_nullStream.rdbuf()); } - void OnTestSuiteEnd(const testing::TestSuite& /*testSuite*/) override { - // Silent - } + void OnTestSuiteEnd(const testing::TestSuite & /*testSuite*/) override { /* Silent */ } - void OnEnvironmentsTearDownStart(const testing::UnitTest& /*unitTest*/) override { - // Silent - } + void OnEnvironmentsTearDownStart(const testing::UnitTest & /*unitTest*/) override { /* Silent */ } - void OnEnvironmentsTearDownEnd(const testing::UnitTest& /*unitTest*/) override { - // Silent - } + void OnEnvironmentsTearDownEnd(const testing::UnitTest & /*unitTest*/) override { /* Silent */ } - void OnTestIterationEnd(const testing::UnitTest& /*unitTest*/, int /*iteration*/) override { - // Silent - } + void OnTestIterationEnd(const testing::UnitTest & /*unitTest*/, int /*iteration*/) override { /* Silent */ } - void OnTestProgramEnd(const testing::UnitTest& unitTest) override { + void OnTestProgramEnd(const testing::UnitTest &unitTest) override + { // Restore stdout/stderr for final output std::cout.rdbuf(m_origCoutBuf); std::cerr.rdbuf(m_origCerrBuf); - + // Calculate elapsed time auto endTime = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast( endTime - m_startTime); - + // Print final grid state printGrid(); - + // Print summary heading - std::cout << "\n" << Color::Bold + std::cout << "\n" + << color::bold << "=========================================\n" << " Test Summary \n" - << "=========================================" << Color::Reset << "\n" + << "=========================================" << color::reset << "\n" << "Total Tests: " << unitTest.total_test_count() << "\n" - << "Passed Tests: " << Color::Green << unitTest.successful_test_count() << Color::Reset << "\n" - << "Failed Tests: " << (unitTest.failed_test_count() > 0 ? Color::Red : "") - << unitTest.failed_test_count() << Color::Reset << "\n" + << "Passed Tests: " << color::green << unitTest.successful_test_count() << color::reset << "\n" + << "Failed Tests: " << (unitTest.failed_test_count() > 0 ? color::red : "") + << unitTest.failed_test_count() << color::reset << "\n" << "Time Elapsed: " << duration.count() << " seconds\n"; - + // Print any failed tests - if (unitTest.failed_test_count() > 0) { - std::cout << "\n" << Color::Bold << Color::Red + if (unitTest.failed_test_count() > 0) + { + std::cout << "\n" + << color::bold << color::red << "=========================================\n" << " Failed Tests \n" - << "=========================================" << Color::Reset << "\n"; - - for (const auto& suite : m_failedTestOutput) { - std::cout << Color::Bold << "Test Suite: " << suite.first << Color::Reset << "\n"; - for (const auto& test : suite.second) { + << "=========================================" << color::reset << "\n"; + + for (const auto &suite : m_failedTestOutput) + { + std::cout << color::bold << "Test Suite: " << suite.first << color::reset << "\n"; + for (const auto &test : suite.second) + { std::cout << " " << test << "\n"; } } } - + // End with a status message - if (unitTest.failed_test_count() == 0) { - std::cout << "\n" << Color::Green << Color::Bold - << "ALL TESTS PASSED!" << Color::Reset << "\n"; - } else { - std::cout << "\n" << Color::Red << Color::Bold - << unitTest.failed_test_count() - << " TESTS FAILED. See details above." << Color::Reset << "\n"; + if (unitTest.failed_test_count() == 0) + { + std::cout << "\n" + << color::green << color::bold + << "ALL TESTS PASSED!" << color::reset << "\n"; + } + else + { + std::cout << "\n" + << color::red << color::bold + << unitTest.failed_test_count() + << " TESTS FAILED. See details above." << color::reset << "\n"; } } private: // Print the current state of all test suites as a grid - void printGrid() { + void printGrid() + { // Move cursor to start of output area - std::cout << "\033[H\033[J"; // Clear screen - std::cout << Color::Bold << "Running " << m_totalTests << " tests... " + std::cout << "\033[H\033[J"; // Clear screen + std::cout << color::bold << "Running " << m_totalTests << " tests... " << "Completed: " << m_completedTests << "/" << m_totalTests << " (P: " << m_passedTests << ", F: " << m_failedTests << ")" - << Color::Reset << "\n"; - + << color::reset << "\n"; + // Print legend - std::cout << Color::Green << "P" << Color::Reset << " - Passed, " - << Color::Red << "F" << Color::Reset << " - Failed, " - << Color::Yellow << "R" << Color::Reset << " - Running, " + std::cout << color::green << "P" << color::reset << " - Passed, " + << color::red << "F" << color::reset << " - Failed, " + << color::yellow << "R" << color::reset << " - Running, " << ". - Not run yet\n\n"; - + // Print each test suite - for (const auto& suite : m_suiteTestStatus) { + for (const auto &suite : m_suiteTestStatus) + { std::string suiteName = suite.first; - const auto& statuses = suite.second; - + const auto &statuses = suite.second; + // Count completed tests in this suite int suiteCompleted = 0; - for (const auto& status : statuses) { - if (status == TestStatus::Passed || status == TestStatus::Failed) { + for (const auto &status : statuses) + if (status == TestStatus::Passed || status == TestStatus::Failed) suiteCompleted++; - } - } - + // Print suite name and status std::cout << std::setw(25) << std::left << suiteName << " "; - + // Print status of each test in the suite - for (const auto& status : statuses) { + for (const auto &status : statuses) + { char statusChar = EMPTY_CHAR; std::string color; - - switch (status) { - case TestStatus::NotRun: - statusChar = NOT_RUN_CHAR; - color = Color::Reset; - break; - case TestStatus::Running: - statusChar = RUNNING_CHAR; - color = Color::Yellow; - break; - case TestStatus::Passed: - statusChar = PASSED_CHAR; - color = Color::Green; - break; - case TestStatus::Failed: - statusChar = FAILED_CHAR; - color = Color::Red; - break; + + switch (status) + { + case TestStatus::NotRun: + statusChar = NOT_RUN_CHAR; + color = color::reset; + break; + case TestStatus::Running: + statusChar = RUNNING_CHAR; + color = color::yellow; + break; + case TestStatus::Passed: + statusChar = PASSED_CHAR; + color = color::green; + break; + case TestStatus::Failed: + statusChar = FAILED_CHAR; + color = color::red; + break; } - - std::cout << color << statusChar << Color::Reset; + + std::cout << color << statusChar << color::reset; } - + // Print completion status for this suite std::cout << " (" << suiteCompleted << "/" << statuses.size() << ")\n"; } } }; -void signalHandler(int signal) { - if(g_origCoutBuf) std::cout.rdbuf(g_origCoutBuf); // Restore original cout buffer - if(g_origCoutBuf) std::cerr.rdbuf(g_origCerrBuf); // Restore original cerr buffer - +void signalHandler(int signal) +{ + if (g_origCoutBuf) + std::cout.rdbuf(g_origCoutBuf); // Restore original cout buffer + if (g_origCerrBuf) + std::cerr.rdbuf(g_origCerrBuf); // Restore original cerr buffer + + std::cerr << "\n\n====== TEST TERMINATED BY SIGNAL ======" << std::endl; std::cerr << "Test crashed with signal " << signal; - if (signal == SIGSEGV) std::cerr << " (SIGSEGV: Segmentation fault)"; - if (signal == SIGABRT) std::cerr << " (SIGABRT: Abort)"; + + // Provide more helpful information based on signal type + if (signal == SIGSEGV) + std::cerr << " (SIGSEGV: Segmentation fault - likely memory access violation)"; + if (signal == SIGABRT) + std::cerr << " (SIGABRT: Abort - likely assertion failure or std::abort call)"; + if (signal == SIGFPE) + std::cerr << " (SIGFPE: Floating point exception)"; + if (signal == SIGILL) + std::cerr << " (SIGILL: Illegal instruction)"; + if (signal == SIGTERM) + std::cerr << " (SIGTERM: Termination request)"; + if (signal == SIGINT) + std::cerr << " (SIGINT: Interrupt)"; + std::cerr << std::endl; + + // Print stack trace information if available +#ifdef __GLIBC__ + void *array[50]; + int size = backtrace(array, 50); + + std::cerr << "\nStack trace:\n"; + backtrace_symbols_fd(array, size, STDERR_FILENO); +#endif + + std::cerr << "\nThis usually indicates a serious error in your code." << std::endl; + std::cerr << "Common causes:" << std::endl; + std::cerr << "- Memory access violation (buffer overflow, use after free)" << std::endl; + std::cerr << "- Concurrency issues (data races, deadlocks)" << std::endl; + std::cerr << "- Assertion failures in your code" << std::endl; + std::cerr << "- Uncaught exceptions in destructors" << std::endl; + std::cerr << "====== END OF CRASH REPORT ======\n\n" + << std::endl; + exit(128 + signal); } -int main(int argc, char **argv) { +int main(int argc, char **argv) +{ // Register signal handlers signal(SIGSEGV, signalHandler); signal(SIGABRT, signalHandler); + signal(SIGFPE, signalHandler); + signal(SIGILL, signalHandler); + signal(SIGTERM, signalHandler); + signal(SIGINT, signalHandler); ::testing::InitGoogleTest(&argc, argv); - + // Remove the default listener - auto& listeners = ::testing::UnitTest::GetInstance()->listeners(); + auto &listeners = ::testing::UnitTest::GetInstance()->listeners(); auto defaultListener = listeners.Release(listeners.default_result_printer()); - + // Add our custom listener listeners.Append(new GridTestListener(defaultListener)); - + return RUN_ALL_TESTS(); } \ No newline at end of file diff --git a/tests/thread_safety_tests.cpp b/tests/thread_safety_tests.cpp deleted file mode 100644 index 5584e1f..0000000 --- a/tests/thread_safety_tests.cpp +++ /dev/null @@ -1,231 +0,0 @@ -#include "gtest/gtest.h" -#include "data_manager.hpp" -#include -#include -#include -#include -#include -#include -#include - -namespace datacoe -{ - - class ThreadSafetyTest : public ::testing::Test - { - protected: - void SetUp() override - { - m_testFilename = "thread_test_data.json"; - // Clean up any leftover files from previous tests - try - { - if (std::filesystem::exists(m_testFilename)) - { - std::filesystem::remove(m_testFilename); - } - } - catch (const std::filesystem::filesystem_error &) - { - // Ignore errors if file doesn't exist - } - } - - void TearDown() override - { - try - { - if (std::filesystem::exists(m_testFilename)) - { - std::filesystem::remove(m_testFilename); - } - } - catch (const std::filesystem::filesystem_error &) - { - // Ignore errors - } - } - - std::string m_testFilename; - }; - - TEST_F(ThreadSafetyTest, ConcurrentReads) - { - // First create data to read - { - DataManager dm; - dm.init(m_testFilename); - dm.setNickName("ThreadTest"); - dm.setHighScore(12345); - ASSERT_TRUE(dm.saveGame()); - } - - constexpr int threadCount = 5; - constexpr int iterationsPerThread = 20; - - std::vector threads; - std::atomic successCount = 0; - - // Launch multiple threads to read simultaneously - for (int t = 0; t < threadCount; t++) - { - threads.emplace_back([&, t]() - { - for (int i = 0; i < iterationsPerThread; i++) { - try { - DataManager dm; - dm.init(m_testFilename); - - const GameData& data = dm.getGameData(); - if (data.getNickName() == "ThreadTest" && data.getHighscore() == 12345) { - successCount++; - } - } catch (const std::exception& e) { - std::cerr << "Thread " << t << " iteration " << i - << " exception: " << e.what() << std::endl; - } - } }); - } - - // Wait for all threads to complete - for (auto &thread : threads) - { - thread.join(); - } - - // All reads should succeed - ASSERT_EQ(successCount, threadCount * iterationsPerThread); - } - - TEST_F(ThreadSafetyTest, ConcurrentWrites) - { - constexpr int threadCount = 5; - constexpr int iterationsPerThread = 20; - - std::vector threads; - std::mutex fileMutex; // Use mutex to avoid file corruption - - // Launch multiple threads to write to the same file - for (int t = 0; t < threadCount; t++) - { - threads.emplace_back([&, t]() - { - for (int i = 0; i < iterationsPerThread; i++) { - std::lock_guard lock(fileMutex); - - try { - DataManager dm; - dm.init(m_testFilename); - - // Include thread and iteration info in nickname - std::string nickname = "Thread" + std::to_string(t) + "_Iter" + std::to_string(i); - dm.setNickName(nickname); - dm.setHighScore(t * 1000 + i); - - ASSERT_TRUE(dm.saveGame()); - - // Verify immediately - DataManager verifyDm; - verifyDm.init(m_testFilename); - ASSERT_EQ(verifyDm.getGameData().getNickName(), nickname); - ASSERT_EQ(verifyDm.getGameData().getHighscore(), t * 1000 + i); - } catch (const std::exception& e) { - std::cerr << "Thread " << t << " iteration " << i - << " exception: " << e.what() << std::endl; - FAIL() << "Exception in thread " << t << ": " << e.what(); - } - } }); - } - - // Wait for all threads to complete - for (auto &thread : threads) - { - thread.join(); - } - - // We can't reliably assert exactly which thread's data will be the last to be written - // since thread scheduling is non-deterministic. So instead, just validate that we have - // valid data that matches our expected format. - DataManager finalDm; - finalDm.init(m_testFilename); - - // Check that the name follows our expected pattern - std::string name = finalDm.getGameData().getNickName(); - ASSERT_TRUE(name.find("Thread") == 0) << "Final data nickname doesn't match expected format"; - - // Check that the score is within the expected range - int score = finalDm.getGameData().getHighscore(); - ASSERT_TRUE(score >= 0 && score < threadCount * 1000 + iterationsPerThread) - << "Final data score outside expected range"; - } - - TEST_F(ThreadSafetyTest, SimultaneousReadWrite) - { - // Initial data - { - DataManager dm; - dm.init(m_testFilename); - dm.setNickName("Initial"); - dm.setHighScore(0); - ASSERT_TRUE(dm.saveGame()); - } - - constexpr int iterations = 100; - std::atomic running{true}; - std::mutex fileMutex; - - // Thread for continuous reading - std::thread readerThread([&]() - { - while (running) { - try { - DataManager dm; - dm.init(m_testFilename); - - // Just read the data - const GameData& data = dm.getGameData(); - (void)data; // Avoid unused variable warning - } catch (const std::exception& e) { - std::cerr << "Reader exception: " << e.what() << std::endl; - } - - // Small delay to avoid overwhelming the system - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } }); - - // Perform writes while the reader is active - for (int i = 0; i < iterations; i++) - { - { - std::lock_guard lock(fileMutex); - - try - { - DataManager dm; - dm.init(m_testFilename); - dm.setNickName("Write" + std::to_string(i)); - dm.setHighScore(i); - ASSERT_TRUE(dm.saveGame()); - } - catch (const std::exception &e) - { - FAIL() << "Writer exception: " << e.what(); - } - } - - // Small delay to give reader a chance - std::this_thread::sleep_for(std::chrono::milliseconds(2)); - } - - // Signal reader to stop and wait for it - running = false; - readerThread.join(); - - // Final state verification - DataManager finalDm; - finalDm.init(m_testFilename); - ASSERT_EQ(finalDm.getGameData().getNickName(), "Write" + std::to_string(iterations - 1)); - ASSERT_EQ(finalDm.getGameData().getHighscore(), iterations - 1); - } - -} // namespace datacoe \ No newline at end of file